// ==UserScript==
// @name Rule34 Favorites Search Gallery
// @namespace bruh3396
// @version 1.17.7
// @description Search, View, and Play Rule34 Favorites (Desktop/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==
class Utils {
static utilitiesHTML = `
<style>
.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;
}
.not-highlightable {
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
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.05, 1.05);
}
.number {
white-space: nowrap;
position: relative;
margin-top: 5px;
border: 1px solid;
padding: 0;
border-radius: 20px;
background-color: white;
>hold-button,
button {
position: relative;
top: 0;
left: 0;
font-size: inherit;
outline: none;
background: none;
cursor: pointer;
border: none;
margin: 0px 8px;
padding: 0;
&::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 200%;
height: 100%;
/* outline: 1px solid greenyellow; */
/* background-color: hotpink; */
}
&:hover {
>span {
color: #0075FF;
}
}
>span {
font-weight: bold;
font-family: Verdana, Geneva, Tahoma, sans-serif;
position: relative;
pointer-events: none;
border: none;
outline: none;
top: 0;
z-index: 5;
font-size: 1.2em !important;
}
&.number-arrow-up {
>span {
transition: left .1s ease;
left: 0;
}
&:hover>span {
left: 3px;
}
}
&.number-arrow-down {
>span {
transition: right .1s ease;
right: 0;
}
&:hover>span {
right: 3px;
}
}
}
>input[type="number"] {
font-size: inherit;
text-align: center;
width: 2ch;
padding: 0;
margin: 0;
font-weight: bold;
padding: 3px;
background: none;
border: none;
&:focus {
outline: none;
}
}
>input[type="number"]::-webkit-outer-spin-button,
>input[type="number"]::-webkit-inner-spin-button {
-webkit-appearance: none;
appearance: none;
margin: 0;
}
}
.fullscreen-icon {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 10010;
pointer-events: none;
width: 30vw;
}
input[type="checkbox"] {
accent-color: #0075FF;
}
.thumb {
>a {
pointer-events: none;
>img {
pointer-events: all;
}
}
}
</style>
`;
static localStorageKeys = {
imageExtensions: "imageExtensions"
};
static settings = {
extensionsFoundBeforeSavingCount: 100
};
static favoritesSearchGalleryContainer = Utils.createFavoritesSearchGalleryContainer();
static idsToRemoveOnReloadLocalStorageKey = "recentlyRemovedIds";
static tagBlacklist = Utils.getTagBlacklist();
static preferencesLocalStorageKey = "preferences";
static 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
},
galleryEnabled: {
set: false,
value: undefined
}
};
static icons = {
delete: "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"feather feather-trash\"><polyline points=\"3 6 5 6 21 6\"></polyline><path d=\"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2\"></path></svg>",
edit: "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"feather feather-edit\"><path d=\"M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7\"></path><path d=\"M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z\"></path></svg>",
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>",
play: "<svg id=\"autoplay-play-button\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 -960 960 960\" fill=\"white\"><path d=\"M320-200v-560l440 280-440 280Zm80-280Zm0 134 210-134-210-134v268Z\" /></svg>",
pause: "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 -960 960 960\" fill=\"white\"><path d=\"M520-200v-560h240v560H520Zm-320 0v-560h240v560H200Zm400-80h80v-400h-80v400Zm-320 0h80v-400h-80v400Zm0-400v400-400Zm320 0v400-400Z\"/></svg>",
changeDirection: "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 -960 960 960\" fill=\"white\"><path d=\"M280-160 80-360l200-200 56 57-103 103h287v80H233l103 103-56 57Zm400-240-56-57 103-103H440v-80h287L624-743l56-57 200 200-200 200Z\"/></svg>",
changeDirectionAlt: "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 -960 960 960\" fill=\"#0075FF\"><path d=\"M280-160 80-360l200-200 56 57-103 103h287v80H233l103 103-56 57Zm400-240-56-57 103-103H440v-80h287L624-743l56-57 200 200-200 200Z\"/></svg>",
tune: "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 -960 960 960\" fill=\"white\"><path d=\"M440-120v-240h80v80h320v80H520v80h-80Zm-320-80v-80h240v80H120Zm160-160v-80H120v-80h160v-80h80v240h-80Zm160-80v-80h400v80H440Zm160-160v-240h80v80h160v80H680v80h-80Zm-480-80v-80h400v80H120Z\"/></svg>",
settings: "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 -960 960 960\" fill=\"white\"><path d=\"m370-80-16-128q-13-5-24.5-12T307-235l-119 50L78-375l103-78q-1-7-1-13.5v-27q0-6.5 1-13.5L78-585l110-190 119 50q11-8 23-15t24-12l16-128h220l16 128q13 5 24.5 12t22.5 15l119-50 110 190-103 78q1 7 1 13.5v27q0 6.5-2 13.5l103 78-110 190-118-50q-11 8-23 15t-24 12L590-80H370Zm70-80h79l14-106q31-8 57.5-23.5T639-327l99 41 39-68-86-65q5-14 7-29.5t2-31.5q0-16-2-31.5t-7-29.5l86-65-39-68-99 42q-22-23-48.5-38.5T533-694l-13-106h-79l-14 106q-31 8-57.5 23.5T321-633l-99-41-39 68 86 64q-5 15-7 30t-2 32q0 16 2 31t7 30l-86 65 39 68 99-42q22 23 48.5 38.5T427-266l13 106Zm42-180q58 0 99-41t41-99q0-58-41-99t-99-41q-59 0-99.5 41T342-480q0 58 40.5 99t99.5 41Zm-2-140Z\"/></svg>"
};
static defaults = {
columnCount: 6,
resultsPerPage: 200
};
static addedFavoriteStatuses = {
error: 0,
alreadyAdded: 1,
notLoggedIn: 2,
success: 3
};
static styles = {
thumbHoverOutline: `
.favorite,
.thumb {
>a,
>span,
>div {
&:hover {
outline: 3px solid #0075FF !important;
}
}
}`,
thumbHoverOutlineDisabled: `
.favorite,
.thumb {
>a,
>span,
>div:not(:has(img.video)) {
&:hover {
outline: none;
}
}
}`
};
static typeableInputs = new Set([
"color",
"email",
"number",
"password",
"search",
"tel",
"text",
"url",
"datetime"
]);
static clickCodes = {
left: 0,
middle: 1,
right: 2
};
static customTags = Utils.loadCustomTags();
static favoriteItemClassName = "favorite";
static imageExtensions = Utils.loadDiscoveredImageExtensions();
/**
* @type {Cooldown}
*/
static imageExtensionAssignmentCooldown;
static recentlyDiscoveredImageExtensionCount = 0;
static extensionDecodings = {
0: "jpg",
1: "png",
2: "jpeg",
3: "gif"
};
static extensionEncodings = {
"jpg": 0,
"png": 1,
"jpeg": 2,
"gif": 3
};
/**
* @type {Function[]}
*/
static staticInitializers = [];
/**
* @type {Boolean}
*/
static get disabled() {
if (Utils.onPostPage()) {
return true;
}
if (Utils.onFavoritesPage()) {
return false;
}
const enabledOnSearchPages = Utils.getPreference("enableOnSearchPages", false);
return !enabledOnSearchPages;
}
/**
* @type {Boolean}
*/
static get enabled() {
return !Utils.disabled;
}
static initialize() {
if (Utils.disabled) {
throw new Error("Favorites Search Gallery disabled");
}
Utils.invokeStaticInitializers();
Utils.removeUnusedScripts();
Utils.insertCommonStyleHTML();
Utils.setupCustomWebComponents();
Utils.toggleFancyImageHovering(true);
Utils.setTheme();
Utils.prepareSearchPage();
Utils.prefetchAdjacentSearchPages();
Utils.setupOriginalImageLinksOnSearchPage();
Utils.initializeImageExtensionAssignmentCooldown();
}
/**
* @param {String} key
* @param {any} value
*/
static 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}
*/
static 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
*/
static setPreference(key, value) {
const preferences = JSON.parse(localStorage.getItem(Utils.preferencesLocalStorageKey) || "{}");
preferences[key] = value;
localStorage.setItem(Utils.preferencesLocalStorageKey, JSON.stringify(preferences));
}
/**
* @param {String} key
* @param {any} defaultValue
* @returns {String | null}
*/
static getPreference(key, defaultValue) {
const preferences = JSON.parse(localStorage.getItem(Utils.preferencesLocalStorageKey) || "{}");
const preference = preferences[key];
if (preference === undefined) {
return defaultValue === undefined ? null : defaultValue;
}
return preference;
}
/**
* @returns {String | null}
*/
static getUserId() {
return Utils.getCookie("user_id");
}
/**
* @returns {String | null}
*/
static getFavoritesPageId() {
const match = (/(?:&|\?)id=(\d+)/).exec(window.location.href);
return match ? match[1] : null;
}
/**
* @returns {Boolean}
*/
static userIsOnTheirOwnFavoritesPage() {
if (!Utils.flags.userIsOnTheirOwnFavoritesPage.set) {
Utils.flags.userIsOnTheirOwnFavoritesPage.value = Utils.getUserId() === Utils.getFavoritesPageId();
Utils.flags.userIsOnTheirOwnFavoritesPage.set = true;
}
return Utils.flags.userIsOnTheirOwnFavoritesPage.value;
}
/**
* @param {Number} value
* @param {Number} min
* @param {Number} max
* @returns {Number}
*/
static clamp(value, min, max) {
if (value <= min) {
return min;
} else if (value >= max) {
return max;
}
return value;
}
/**
* @param {Number} milliseconds
* @returns
*/
static sleep(milliseconds) {
return new Promise(resolve => setTimeout(resolve, milliseconds));
}
/**
* @param {Boolean} value
*/
static 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}
*/
static getRemoveFavoriteButtonFromThumb(thumb) {
return thumb.querySelector(".remove-favorite-button");
}
/**
* @param {HTMLElement} thumb
* @returns {String | null}
*/
static getAddFavoriteButtonFromThumb(thumb) {
return thumb.querySelector(".add-favorite-button");
}
/**
* @param {HTMLImageElement} image
*/
static removeTitleFromImage(image) {
if (image.hasAttribute("title")) {
image.setAttribute("tags", image.title);
image.removeAttribute("title");
}
}
/**
* @param {HTMLImageElement} image
* @returns {HTMLElement}
*/
static getThumbFromImage(image) {
const className = Utils.onSearchPage() ? "thumb" : Utils.favoriteItemClassName;
return image.closest(`.${className}`);
}
/**
* @param {HTMLElement} thumb
* @returns {HTMLImageElement}
*/
static getImageFromThumb(thumb) {
return thumb.querySelector("img");
}
/**
* @returns {HTMLElement[]}
*/
static getAllThumbs() {
const className = Utils.onSearchPage() ? "thumb" : Utils.favoriteItemClassName;
return Array.from(document.getElementsByClassName(className));
}
/**
* @param {HTMLElement} thumb
* @returns {String}
*/
static getOriginalImageURLFromThumb(thumb) {
return Utils.getOriginalImageURL(Utils.getImageFromThumb(thumb).src);
}
/**
* @param {String} thumbURL
* @returns {String}
*/
static getOriginalImageURL(thumbURL) {
return thumbURL
.replace("thumbnails", "/images")
.replace("thumbnail_", "")
.replace("us.rule34", "rule34");
}
/**
* @param {String} imageURL
* @returns {String}
*/
static getExtensionFromImageURL(imageURL) {
try {
return (/\.(png|jpg|jpeg|gif|mp4)/g).exec(imageURL)[1];
} catch (error) {
return "jpg";
}
}
/**
* @param {String} originalImageURL
* @returns {String}
*/
static getThumbURL(originalImageURL) {
return originalImageURL
.replace(/\/images\/\/(\d+)\//, "thumbnails/$1/thumbnail_")
.replace(/(?:gif|jpeg|png)/, "jpg")
.replace("us.rule34", "rule34");
}
/**
* @param {HTMLElement | Post} thumb
* @returns {Set.<String>}
*/
static getTagsFromThumb(thumb) {
if (Utils.onSearchPage()) {
const image = Utils.getImageFromThumb(thumb);
const tags = image.hasAttribute("tags") ? image.getAttribute("tags") : image.title;
return Utils.convertToTagSet(tags);
}
const post = Post.allPosts.get(thumb.id);
return post === undefined ? new Set() : new Set(post.tagSet);
}
/**
* @param {String} tag
* @param {Set.<String>} tags
* @returns
*/
static includesTag(tag, tags) {
return tags.has(tag);
}
/**
* @param {HTMLElement | Post} thumb
* @returns {Boolean}
*/
static isVideo(thumb) {
const tags = Utils.getTagsFromThumb(thumb);
return tags.has("video") || tags.has("mp4");
}
/**
* @param {HTMLElement | Post} thumb
* @returns {Boolean}
*/
static isGif(thumb) {
if (Utils.isVideo(thumb)) {
return false;
}
const tags = Utils.getTagsFromThumb(thumb);
return tags.has("gif") || tags.has("animated") || tags.has("animated_png") || Utils.hasGifAttribute(thumb);
}
/**
* @param {HTMLElement | Post} thumb
* @returns {Boolean}
*/
static hasGifAttribute(thumb) {
if (thumb instanceof Post) {
return false;
}
return Utils.getImageFromThumb(thumb).hasAttribute("gif");
}
/**
* @param {HTMLElement | Post} thumb
* @returns {Boolean}
*/
static isImage(thumb) {
return !Utils.isVideo(thumb) && !Utils.isGif(thumb);
}
/**
* @param {Number} maximum
* @returns {Number}
*/
static getRandomInteger(maximum) {
return Math.floor(Math.random() * maximum);
}
/**
* @param {any[]} array
*/
static shuffleArray(array) {
let maxIndex = array.length;
let randomIndex;
while (maxIndex > 0) {
randomIndex = Utils.getRandomInteger(maxIndex);
maxIndex -= 1;
[
array[maxIndex],
array[randomIndex]
] = [
array[randomIndex],
array[maxIndex]
];
}
}
/**
* @param {String} tags
* @returns {String}
*/
static negateTags(tags) {
return tags.replace(/(\S+)/g, "-$1");
}
/**
* @param {HTMLInputElement | HTMLTextAreaElement} input
* @returns {HTMLDivElement | null}
*/
static getAwesompleteFromInput(input) {
const awesomplete = input.parentElement;
if (awesomplete === null || awesomplete.className !== "awesomplete") {
return null;
}
return awesomplete;
}
/**
* @param {HTMLInputElement | HTMLTextAreaElement} input
* @returns {Boolean}
*/
static awesompleteIsVisible(input) {
const awesomplete = Utils.getAwesompleteFromInput(input);
if (awesomplete === null) {
return false;
}
const awesompleteSuggestions = awesomplete.querySelector("ul");
return awesompleteSuggestions !== null && !awesompleteSuggestions.hasAttribute("hidden");
}
/**
*
* @param {HTMLInputElement | HTMLTextAreaElement} input
* @returns
*/
static awesompleteIsUnselected(input) {
const awesomplete = Utils.getAwesompleteFromInput(input);
if (awesomplete === null) {
return true;
}
if (!Utils.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
*/
static 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
* @param {String} optionHint
* @returns {HTMLElement | null}
*/
static createFavoritesOption(optionId, optionText, optionTitle, optionIsChecked, onOptionChanged, optionIsVisible, optionHint = "") {
const id = Utils.onMobileDevice() ? "favorite-options" : "dynamic-favorite-options";
const placeToInsert = document.getElementById(id);
const checkboxId = `${optionId}-checkbox`;
if (placeToInsert === null) {
return null;
}
if (optionIsVisible === undefined || optionIsVisible) {
optionIsVisible = "block";
} else {
optionIsVisible = "none";
}
placeToInsert.insertAdjacentHTML("beforeend", `
<div id="${optionId}" style="display: ${optionIsVisible}">
<label class="checkbox" title="${optionTitle}">
<input id="${checkboxId}" type="checkbox"> ${optionText}<span class="option-hint"> ${optionHint}</span></label>
</div>
`);
const newOptionsCheckbox = document.getElementById(checkboxId);
newOptionsCheckbox.checked = optionIsChecked;
newOptionsCheckbox.onchange = onOptionChanged;
return document.getElementById(optionId);
}
/**
* @returns {Boolean}
*/
static onSearchPage() {
if (!Utils.flags.onSearchPage.set) {
Utils.flags.onSearchPage.value = location.href.includes("page=post&s=list");
Utils.flags.onSearchPage.set = true;
}
return Utils.flags.onSearchPage.value;
}
/**
* @returns {Boolean}
*/
static onFavoritesPage() {
if (!Utils.flags.onFavoritesPage.set) {
Utils.flags.onFavoritesPage.value = location.href.includes("page=favorites");
Utils.flags.onFavoritesPage.set = true;
}
return Utils.flags.onFavoritesPage.value;
}
/**
* @returns {Boolean}
*/
static onPostPage() {
if (!Utils.flags.onPostPage.set) {
Utils.flags.onPostPage.value = location.href.includes("page=post&s=view");
Utils.flags.onPostPage.set = true;
}
return Utils.flags.onPostPage.value;
}
/**
* @returns {String[]}
*/
static getIdsToDeleteOnReload() {
return JSON.parse(localStorage.getItem(Utils.idsToRemoveOnReloadLocalStorageKey)) || [];
}
/**
* @param {String} postId
*/
static setIdToBeRemovedOnReload(postId) {
const idsToRemoveOnReload = Utils.getIdsToDeleteOnReload();
idsToRemoveOnReload.push(postId);
localStorage.setItem(Utils.idsToRemoveOnReloadLocalStorageKey, JSON.stringify(idsToRemoveOnReload));
}
static clearIdsToDeleteOnReload() {
localStorage.removeItem(Utils.idsToRemoveOnReloadLocalStorageKey);
}
/**
* @param {String} html
* @param {String} id
*/
static insertStyleHTML(html, id) {
const style = document.createElement("style");
style.textContent = html.replace("<style>", "").replace("</style>", "");
if (id !== undefined) {
id += "-fsg-style";
const oldStyle = document.getElementById(id);
if (oldStyle !== null) {
oldStyle.remove();
}
style.id = id;
}
document.head.appendChild(style);
}
static getTagDistribution() {
const images = Utils.getAllThumbs().map(thumb => Utils.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 = Utils.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}}
*/
static 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]
}));
}
static insertCommonStyleHTML() {
Utils.insertStyleHTML(Utils.utilitiesHTML, "common");
Utils.toggleThumbHoverOutlines(false);
setTimeout(() => {
if (Utils.onSearchPage()) {
Utils.removeInlineImgStyles();
}
Utils.configureVideoOutlines();
}, 100);
}
/**
* @param {Boolean} value
*/
static toggleFancyImageHovering(value) {
if (Utils.onMobileDevice() || Utils.onSearchPage()) {
value = false;
}
let html = "";
if (value) {
html = `
#favorites-search-gallery-content {
padding: 40px 40px 30px !important;
grid-gap: 2.5em !important;
}
.favorite,
.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.05, 1.05);
z-index: 10;
img {
outline: none !important;
}
&::after {
opacity: 1;
}
}
}
}
`;
}
Utils.insertStyleHTML(html, "fancy-image-hovering");
}
static configureVideoOutlines() {
const size = Utils.onMobileDevice() ? 2 : 3;
const videoSelector = Utils.onFavoritesPage() ? "&:has(img.video)" : ">img.video";
const gifSelector = Utils.onFavoritesPage() ? "&:has(img.gif)" : ">img.gif";
Utils.insertStyleHTML(`
.favorite, .thumb {
>a,
>div {
${videoSelector} {
outline: ${size}px solid blue;
}
${gifSelector} {
outline: 2px solid hotpink;
}
}
}
`, "video-gif-borders");
}
static removeInlineImgStyles() {
for (const image of document.getElementsByTagName("img")) {
image.removeAttribute("style");
}
}
static setTheme() {
setTimeout(() => {
if (Utils.usingDarkTheme()) {
for (const element of document.querySelectorAll(".light-green-gradient")) {
element.classList.remove("light-green-gradient");
element.classList.add("dark-green-gradient");
Utils.insertStyleHTML(`
input[type=number] {
background-color: #303030;
color: white;
}
.number {
background-color: #303030;
>hold-button,
button {
color: white;
}
}
#favorites-pagination-container {
>button {
border: 1px solid white !important;
color: white !important;
}
}
`, "dark-theme");
}
}
}, 10);
}
/**
* @param {String} content
* @returns {Blob | MediaSource}
*/
static getWorkerURL(content) {
return URL.createObjectURL(new Blob([content], {
type: "text/javascript"
}));
}
static prefetchAdjacentSearchPages() {
if (!Utils.onSearchPage()) {
return;
}
const id = "search-page-prefetch";
const alreadyPrefetched = document.getElementById(id) !== null;
if (alreadyPrefetched) {
return;
}
const container = document.createElement("div");
try {
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(Utils.createPrefetchLink(sibling.href));
}
}
container.id = "search-page-prefetch";
document.head.appendChild(container);
} catch (error) {
console.error(error);
}
}
/**
* @param {String} url
* @returns {HTMLLinkElement}
*/
static createPrefetchLink(url) {
const link = document.createElement("link");
link.rel = "prefetch";
link.href = url;
return link;
}
/**
* @returns {String}
*/
static getTagBlacklist() {
let tags = Utils.getCookie("tag_blacklist", "");
for (let i = 0; i < 3; i += 1) {
tags = decodeURIComponent(tags).replace(/(?:^| )-/, "");
}
return tags;
}
/**
* @returns {Boolean}
*/
static galleryEnabled() {
if (!Utils.flags.galleryEnabled.set) {
Utils.flags.galleryEnabled.value = document.getElementById("gallery-container") !== null;
Utils.flags.galleryEnabled.set = true;
}
return Utils.flags.galleryEnabled.value;
}
/**
* @param {String} word
* @returns {String}
*/
static capitalize(word) {
return word.charAt(0).toUpperCase() + word.slice(1);
}
/**
* @param {Number} number
* @returns {Number}
*/
static roundToTwoDecimalPlaces(number) {
return Math.round((number + Number.EPSILON) * 100) / 100;
}
/**
* @param {Number} n
* @param {Number} number
*/
static roundToNDecimalPlaces(n, number) {
const x = 10 ** n;
return Math.round((number + Number.EPSILON) * x) / x;
}
/**
* @returns {Boolean}
*/
static usingDarkTheme() {
return Utils.getCookie("theme", "") === "dark";
}
/**
* @param {Event} event
* @returns {Boolean}
*/
static enteredOverCaptionTag(event) {
return event.relatedTarget !== null && event.relatedTarget.classList.contains("caption-tag");
}
/**
* @param {String[]} postId
* @param {Boolean} endingAnimation
* @param {Boolean} smoothTransition
*/
static scrollToThumb(postId, endingAnimation, smoothTransition) {
const element = document.getElementById(postId);
const elementIsNotAThumb = element === null || (!element.classList.contains("thumb") && !element.classList.contains(Utils.favoriteItemClassName));
if (elementIsNotAThumb) {
return;
}
const rect = element.getBoundingClientRect();
const menu = document.getElementById("favorites-search-gallery-menu");
const favoritesSearchHeight = menu === null ? 0 : menu.getBoundingClientRect().height;
window.scroll({
top: rect.top + window.scrollY + (rect.height / 2) - (window.innerHeight / 2) - (favoritesSearchHeight / 2),
behavior: smoothTransition ? "smooth" : "instant"
});
if (!endingAnimation) {
return;
}
const image = Utils.getImageFromThumb(element);
image.classList.add("found");
setTimeout(() => {
image.classList.remove("found");
}, 2000);
}
/**
* @param {HTMLElement} thumb
*/
static assignContentType(thumb) {
const image = Utils.getImageFromThumb(thumb);
const tagAttribute = image.hasAttribute("tags") ? "tags" : "title";
const tags = image.getAttribute(tagAttribute);
Utils.setContentType(image, Utils.getContentType(tags));
}
/**
* @param {HTMLImageElement} image
* @param {String} type
*/
static setContentType(image, type) {
image.classList.remove("image");
image.classList.remove("gif");
image.classList.remove("video");
image.classList.add(type);
}
/**
* @param {String} tags
* @returns {String}
*/
static 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";
}
static correctMisspelledTags(tags) {
if ((/vide(?:\s|$)/).test(tags)) {
tags += " video";
}
return tags;
}
/**
* @param {String} searchQuery
* @returns {{orGroups: String[][], remainingSearchTags: String[]}}
*/
static extractTagGroups(searchQuery) {
searchQuery = searchQuery.toLowerCase();
const orRegex = /(?:^|\s+)\(\s+((?:\S+)(?:(?:\s+~\s+)\S+)*)\s+\)/g;
const orGroups = Array.from(Utils.removeExtraWhiteSpace(searchQuery)
.matchAll(orRegex))
.map((orGroup) => orGroup[1].split(" ~ "));
const remainingSearchTags = Utils.removeExtraWhiteSpace(searchQuery
.replace(orRegex, ""))
.split(" ")
.filter((searchTag) => searchTag !== "");
return {
orGroups,
remainingSearchTags
};
}
/**
* @param {String} string
* @returns {String}
*/
static removeExtraWhiteSpace(string) {
return string.trim().replace(/\s\s+/g, " ");
}
/**
* @param {String} string
* @param {String} replacement
* @returns {String}
*/
static replaceLineBreaks(string, replacement = "") {
return string.replace(/(\r\n|\n|\r)/gm, replacement);
}
/**
*
* @param {HTMLImageElement} image
* @returns {Boolean}
*/
static imageIsLoaded(image) {
return image.complete || image.naturalWidth !== 0;
}
/**
* @returns {Boolean}
*/
static usingFirefox() {
if (!Utils.flags.usingFirefox.set) {
Utils.flags.usingFirefox.value = navigator.userAgent.toLowerCase().includes("firefox");
Utils.flags.usingFirefox.set = true;
}
return Utils.flags.usingFirefox.value;
}
/**
* @returns {Boolean}
*/
static onMobileDevice() {
if (!Utils.flags.onMobileDevice.set) {
Utils.flags.onMobileDevice.value = (/iPhone|iPad|iPod|Android/i).test(navigator.userAgent);
Utils.flags.onMobileDevice.set = true;
}
return Utils.flags.onMobileDevice.value;
}
/**
* @returns {Number}
*/
static getPerformanceProfile() {
return parseInt(Utils.getPreference("performanceProfile", 0));
}
/**
* @param {String} tagName
* @returns {Promise.<Boolean>}
*/
static 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
*/
static openSearchPage(searchQuery) {
window.open(`https://rule34.xxx/index.php?page=post&s=list&tags=${encodeURIComponent(searchQuery)}`);
}
/**
* @param {Map} map
* @returns {Object}
*/
static mapToObject(map) {
return Array.from(map).reduce((object, [key, value]) => {
object[key] = value;
return object;
}, {});
}
/**
* @param {Object} object
* @returns {Map}
*/
static objectToMap(object) {
return new Map(Object.entries(object));
}
/**
* @param {String} string
* @returns {Boolean}
*/
static isNumber(string) {
return (/^\d+$/).test(string);
}
/**
* @param {String} id
* @returns {Promise.<Number>}
*/
static 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 Utils.addedFavoriteStatuses.error;
});
}
/**
* @param {String} id
*/
static removeFavorite(id) {
Utils.setIdToBeRemovedOnReload(id);
fetch(`https://rule34.xxx/index.php?page=favorites&s=delete&id=${id}`);
}
/**
* @param {HTMLInputElement | HTMLTextAreaElement} input
* @param {String} suggestion
*/
static 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 = Utils.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
*/
static hideAwesomplete(input) {
Utils.getAwesompleteFromInput(input).querySelector("ul").setAttribute("hidden", "");
}
/**
* @param {String} svg
* @param {Number} duration
*/
static 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);
document.body.appendChild(svgOverlay);
setTimeout(() => {
svgOverlay.remove();
}, duration);
}
/**
* @param {String} svg
* @returns {String}
*/
static createObjectURLFromSvg(svg) {
const blob = new Blob([svg], {
type: "image/svg+xml"
});
return URL.createObjectURL(blob);
}
/**
* @param {HTMLElement} element
* @returns {Boolean}
*/
static isTypeableInput(element) {
const tagName = element.tagName.toLowerCase();
if (tagName === "textarea") {
return true;
}
if (tagName === "input") {
return Utils.typeableInputs.has(element.getAttribute("type"));
}
return false;
}
/**
* @param {KeyboardEvent} event
* @returns {Boolean}
*/
static isHotkeyEvent(event) {
return !event.repeat && !Utils.isTypeableInput(event.target);
}
/**
* @param {Set} a
* @param {Set} b
* @returns {Set}
*/
static union(a, b) {
const c = new Set(a);
for (const element of b.values()) {
c.add(element);
}
return c;
}
/**
* @param {Set} a
* @param {Set} b
* @returns {Set}
*/
static difference(a, b) {
const c = new Set(a);
for (const element of b.values()) {
c.delete(element);
}
return c;
}
static removeUnusedScripts() {
if (!Utils.onFavoritesPage()) {
return;
}
const scripts = Array.from(document.querySelectorAll("script"));
for (const script of scripts) {
if ((/(?:fluidplayer|awesomplete)/).test(script.src || "")) {
script.remove();
}
}
}
/**
* @param {String} tagString
* @returns {Set.<String>}
*/
static convertToTagSet(tagString) {
tagString = Utils.removeExtraWhiteSpace(tagString);
if (tagString === "") {
return new Set();
}
return new Set(tagString.split(" ").sort());
}
/**
* @param {Set.<String>} tagSet
* @returns {String}
*/
static convertToTagString(tagSet) {
if (tagSet.size === 0) {
return "";
}
return Array.from(tagSet).join(" ");
}
/**
* @returns {String | null}
*/
static getPostPageId() {
const match = (/id=(\d+)/).exec(window.location.href);
return match === null ? null : match[1];
}
/**
* @param {String} searchTag
* @param {String[]} tags
* @returns {Boolean}
*/
static tagsMatchWildcardSearchTag(searchTag, tags) {
try {
const wildcardRegex = new RegExp(`^${searchTag.replaceAll(/\*/g, ".*")}$`);
return tags.some(tag => wildcardRegex.test(tag));
} catch {
return false;
}
}
static setupCustomWebComponents() {
Utils.setupCustomNumberWebComponents();
}
static async setupCustomNumberWebComponents() {
await Utils.sleep(400);
const numberComponents = Array.from(document.querySelectorAll(".number"));
for (const element of numberComponents) {
const numberComponent = new NumberComponent(element);
}
}
/**
* @param {Number} milliseconds
* @returns {Number}
*/
static millisecondsToSeconds(milliseconds) {
return Utils.roundToTwoDecimalPlaces(milliseconds / 1000);
}
/**
* @returns {Set.<String>}
*/
static loadCustomTags() {
return new Set(JSON.parse(localStorage.getItem("customTags")) || []);
}
/**
* @param {String} tags
*/
static async setCustomTags(tags) {
for (const tag of Utils.removeExtraWhiteSpace(tags).split(" ")) {
if (tag === "" || Utils.customTags.has(tag)) {
continue;
}
const isAnOfficialTag = await Utils.isOfficialTag(tag);
if (!isAnOfficialTag) {
Utils.customTags.add(tag);
}
}
localStorage.setItem("customTags", JSON.stringify(Array.from(Utils.customTags)));
}
/**
* @returns {String[]}
*/
static getSavedSearchValues() {
return Array.from(document.getElementsByClassName("save-search-label"))
.map(element => element.innerText);
}
/**
* @param {{label: String, value: String, type: String}[]} officialTags
* @param {String} searchQuery
* @returns {{label: String, value: String, type: String}[]}
*/
static addCustomTagsToAutocompleteList(officialTags, searchQuery) {
const customTags = Array.from(Utils.customTags);
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;
}
/**
* @param {String} searchTag
* @param {String} savedSearch
* @returns {Boolean}
*/
static savedSearchMatchesSearchTag(searchTag, savedSearch) {
const sanitizedSavedSearch = Utils.removeExtraWhiteSpace(savedSearch.replace(/[~())]/g, ""));
const savedSearchTagList = sanitizedSavedSearch.split(" ");
for (const savedSearchTag of savedSearchTagList) {
if (savedSearchTag.startsWith(searchTag)) {
return true;
}
}
return false;
}
/**
* @param {String} tag
* @returns {String}
*/
static removeStartingHyphen(tag) {
return tag.replace(/^-/, "");
}
/**
* @param {String} searchTag
* @returns {{label: String, value: String, type: String}[]}
*/
static getSavedSearchesForAutocompleteList(searchTag) {
const minimumSearchTagLength = 3;
if (searchTag.length < minimumSearchTagLength) {
return [];
}
const maxMatchedSavedSearches = 5;
const matchedSavedSearches = [];
let i = 0;
for (const savedSearch of Utils.getSavedSearchValues()) {
if (Utils.savedSearchMatchesSearchTag(searchTag, savedSearch)) {
matchedSavedSearches.push({
label: `${savedSearch}`,
value: `${searchTag}_saved_search ${savedSearch}`,
type: "saved"
});
i += 1;
}
if (matchedSavedSearches.length > maxMatchedSavedSearches) {
break;
}
}
return matchedSavedSearches;
}
static removeSavedSearchPrefix(suggestion) {
return suggestion.replace(/^\S+_saved_search /, "");
}
/**
* @param {Boolean} value
*/
static toggleThumbHoverOutlines(value) {
// insertStyleHTML(value ? STYLES.thumbHoverOutlineDisabled : STYLES.thumbHoverOutline, "thumb-hover-outlines");
}
/**
* @param {Number} timestamp
* @returns {String}
*/
static convertTimestampToDate(timestamp) {
const date = new Date(timestamp);
const day = date.getDate();
const month = date.getMonth() + 1;
const year = date.getFullYear();
return `${year}-${month}-${day}`;
}
/**
* @returns {String}
*/
static getSortingMethod() {
const sortingMethodSelect = document.getElementById("sorting-method");
return sortingMethodSelect === null ? "default" : sortingMethodSelect.value;
}
/**
* @returns {HTMLDivElement}
*/
static createFavoritesSearchGalleryContainer() {
const container = document.createElement("div");
container.id = "favorites-search-gallery";
document.body.appendChild(container);
return container;
}
/**
* @param {HTMLElement} element
* @param {InsertPosition} position
* @param {String} html
*/
static insertHTMLAndExtractStyle(element, position, html) {
const dom = new DOMParser().parseFromString(html, "text/html");
const styles = Array.from(dom.querySelectorAll("style"));
for (const style of styles) {
Utils.insertStyleHTML(style.innerHTML);
style.remove();
}
element.insertAdjacentHTML(position, dom.body.innerHTML);
}
/**
* @param {InsertPosition} position
* @param {String} html
*/
static insertFavoritesSearchGalleryHTML(position, html) {
Utils.insertHTMLAndExtractStyle(Utils.favoritesSearchGalleryContainer, position, html);
}
/**
* @param {String} str
* @returns {String}
*/
static removeNonNumericCharacters(str) {
return str.replaceAll(/\D/g, "");
}
/**
* @param {HTMLElement} thumb
* @returns {String}
*/
static getIdFromThumb(thumb) {
const id = thumb.getAttribute("id");
if (id !== null) {
return Utils.removeNonNumericCharacters(id);
}
const anchor = thumb.querySelector("a");
if (anchor !== null && anchor.hasAttribute("id")) {
return Utils.removeNonNumericCharacters(anchor.id);
}
if (anchor !== null && anchor.hasAttribute("href")) {
const match = (/id=(\d+)$/).exec(anchor.href);
if (match !== null) {
return match[1];
}
}
const image = thumb.querySelector("img");
const match = (/\?(\d+)$/).exec(image.src);
return match[1];
}
static 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(FavoritesDatabaseWrapper.databaseName);
}
}
/**
* @param {String} id
* @returns {String}
*/
static getPostPageURL(id) {
return `https://rule34.xxx/index.php?page=post&s=view&id=${id}`;
}
/**
* @param {String} id
*/
static openPostInNewTab(id) {
window.open(Utils.getPostPageURL(id), "_blank");
}
/**
* @param {Function} initializer
*/
static addStaticInitializer(initializer) {
Utils.staticInitializers.push(initializer);
}
static invokeStaticInitializers() {
for (const initializer of Utils.staticInitializers) {
initializer();
}
Utils.staticInitializers = null;
}
/**
* @returns {Number}
*/
static loadAllowedRatings() {
return parseInt(Utils.getPreference("allowedRatings", 7));
}
/**
* @param {Set} a
* @param {Set} b
* @returns {Set}
*/
static symmetricDifference(a, b) {
return Utils.union(Utils.difference(a, b), Utils.difference(b, a));
}
static clearOriginalFavoritesPage() {
const thumbs = Array.from(document.getElementsByClassName("thumb"));
let content = document.getElementById("content");
if (content === null && thumbs.length > 0) {
content = thumbs[0].closest("body>div");
}
if (content !== null) {
content.remove();
}
setTimeout(() => {
dispatchEvent(new CustomEvent("originalFavoritesCleared", {
detail: thumbs
}));
}, 1000);
}
/**
* @param {String} id
* @returns {String}
*/
static getPostAPIURL(id) {
return `https://api.rule34.xxx//index.php?page=dapi&s=post&q=index&id=${id}`;
}
/**
* @returns {Promise<String>}
*/
static getImageExtensionFromThumb(thumb) {
if (Utils.isVideo(thumb)) {
return "mp4";
}
if (Utils.isGif(thumb)) {
return "gif";
}
if (Utils.extensionIsKnown(thumb.id)) {
return Utils.getImageExtension(thumb.id);
}
return Utils.fetchImageExtension(thumb);
}
/**
* @param {HTMLElement} thumb
* @returns {Promise<String>}
*/
static fetchImageExtension(thumb) {
return fetch(Utils.getPostAPIURL(thumb.id))
.then((response) => {
return response.text();
})
.then((html) => {
const dom = new DOMParser().parseFromString(html, "text/html");
const metadata = dom.querySelector("post");
const extension = Utils.getExtensionFromImageURL(metadata.getAttribute("file_url"));
Utils.assignImageExtension(thumb.id, extension);
return extension;
})
.catch((error) => {
console.error(error);
return "jpg";
});
}
/**
* @param {HTMLElement} thumb
* @returns {Promise<String>}
*/
static async getOriginalImageURLWithExtension(thumb) {
const extension = await Utils.getImageExtensionFromThumb(thumb);
return Utils.getOriginalImageURL(thumb.querySelector("img").src).replace(".jpg", `.${extension}`);
}
/**
* @param {HTMLElement} thumb
*/
static async openOriginalImageInNewTab(thumb) {
try {
const imageURL = await Utils.getOriginalImageURLWithExtension(thumb);
window.open(imageURL);
} catch (error) {
console.error(error);
}
}
/**
* @returns {String}
*/
static getSearchPageAPIURL() {
const postsPerPage = 42;
const apiURL = `https://api.rule34.xxx/index.php?page=dapi&s=post&q=index&limit=${postsPerPage}`;
let blacklistedTags = ` ${Utils.negateTags(Utils.tagBlacklist)}`.replace(/\s-/g, "+-");
let pageNumber = (/&pid=(\d+)/).exec(location.href);
let searchTags = (/&tags=([^&]*)/).exec(location.href);
pageNumber = pageNumber === null ? 0 : Math.floor(parseInt(pageNumber[1]) / postsPerPage);
searchTags = searchTags === null ? "" : searchTags[1];
if (searchTags === "all") {
searchTags = "";
blacklistedTags = "";
}
return `${apiURL}&tags=${searchTags}${blacklistedTags}&pid=${pageNumber}`;
}
static findImageExtensionsOnSearchPage() {
const searchPageAPIURL = Utils.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 tags = post.getAttribute("tags");
const id = post.getAttribute("id");
const originalImageURL = post.getAttribute("file_url");
const tagSet = Utils.convertToTagSet(tags);
const thumb = document.getElementById(id);
if (!tagSet.has("video") && originalImageURL.endsWith("mp4") && thumb !== null) {
const image = Utils.getImageFromThumb(thumb);
image.setAttribute("tags", `${image.getAttribute("tags")} video`);
Utils.setContentType(image, "video");
} else if (!tagSet.has("gif") && originalImageURL.endsWith("gif") && thumb !== null) {
const image = Utils.getImageFromThumb(thumb);
image.setAttribute("tags", `${image.getAttribute("tags")} gif`);
Utils.setContentType(image, "gif");
}
const isAnImage = Utils.getContentType(tags) === "image";
const isBlacklisted = originalImageURL === "https://api-cdn.rule34.xxx/images//";
if (!isAnImage || isBlacklisted) {
continue;
}
const extension = Utils.getExtensionFromImageURL(originalImageURL);
Utils.assignImageExtension(id, extension);
}
})
.catch((error) => {
console.error(error);
});
}
static async setupOriginalImageLinksOnSearchPage() {
if (!Utils.onSearchPage()) {
return;
}
if (Gallery.disabled) {
await Utils.findImageExtensionsOnSearchPage();
Utils.setupOriginalImageLinksOnSearchPageHelper();
} else {
window.addEventListener("foundExtensionsOnSearchPage", () => {
Utils.setupOriginalImageLinksOnSearchPageHelper();
}, {
once: true
});
}
}
static async setupOriginalImageLinksOnSearchPageHelper() {
try {
for (const thumb of Utils.getAllThumbs()) {
await Utils.setupOriginalImageLinkOnSearchPage(thumb);
}
} catch (error) {
console.error(error);
}
}
/**
* @param {HTMLElement} thumb
*/
static async setupOriginalImageLinkOnSearchPage(thumb) {
const anchor = thumb.querySelector("a");
const imageURL = await Utils.getOriginalImageURLWithExtension(thumb);
const thumbURL = anchor.href;
anchor.href = imageURL;
anchor.onclick = (event) => {
if (!event.ctrlKey) {
event.preventDefault();
}
};
anchor.onmousedown = (event) => {
if (!event.ctrlKey) {
if (event.button === Utils.clickCodes.left && Gallery.disabled) {
document.location = thumbURL;
} else if (event.button === Utils.clickCodes.middle) {
window.open(thumbURL);
}
event.preventDefault();
}
};
}
static prepareSearchPage() {
if (!Utils.onSearchPage()) {
return;
}
for (const thumb of Utils.getAllThumbs()) {
Utils.removeTitleFromImage(Utils.getImageFromThumb(thumb));
Utils.assignContentType(thumb);
thumb.id = Utils.removeNonNumericCharacters(Utils.getIdFromThumb(thumb));
}
}
/**
* @returns {Object.<String, Number>}
*/
static loadDiscoveredImageExtensions() {
return JSON.parse(localStorage.getItem(Utils.localStorageKeys.imageExtensions)) || {};
}
/**
* @param {String | Number} id
* @returns {String}
*/
static getImageExtension(id) {
return Utils.extensionDecodings[Utils.imageExtensions[parseInt(id)]];
}
/**
* @param {String | Number} id
* @param {String} extension
*/
static setImageExtension(id, extension) {
Utils.imageExtensions[parseInt(id)] = Utils.extensionEncodings[extension];
}
/**
* @param {String} id
* @returns {Boolean}
*/
static extensionIsKnown(id) {
return Utils.getImageExtension(id) !== undefined;
}
static updateStoredImageExtensions() {
Utils.recentlyDiscoveredImageExtensionCount += 1;
if (Utils.recentlyDiscoveredImageExtensionCount >= Utils.settings.extensionsFoundBeforeSavingCount) {
this.storeAllImageExtensions();
}
}
static storeAllImageExtensions() {
if (!Utils.onFavoritesPage()) {
return;
}
Utils.recentlyDiscoveredImageExtensionCount = 0;
localStorage.setItem(Utils.localStorageKeys.imageExtensions, JSON.stringify(Utils.imageExtensions));
}
static isAnAnimatedExtension(extension) {
return extension === "mp4" || extension === "gif";
}
/**
* @param {String} id
* @param {String} extension
*/
static assignImageExtension(id, extension) {
if (Utils.extensionIsKnown(id) || Utils.isAnAnimatedExtension(extension)) {
return;
}
Utils.imageExtensionAssignmentCooldown.restart();
Utils.setImageExtension(id, extension);
Utils.updateStoredImageExtensions();
}
static initializeImageExtensionAssignmentCooldown() {
Utils.imageExtensionAssignmentCooldown = new Cooldown(1000);
Utils.imageExtensionAssignmentCooldown.onCooldownEnd = () => {
if (Utils.recentlyDiscoveredImageExtensionCount > 0) {
Utils.storeAllImageExtensions();
}
};
}
}
class HoldButton extends HTMLElement {
static {
Utils.addStaticInitializer(() => {
customElements.define("hold-button", HoldButton);
});
}
/**
* @type {Number}
*/
static defaultPollingTime = 100;
/**
* @type {Number}
*/
static minPollingTime = 40;
/**
* @type {Number}
*/
static maxPollingTime = 500;
/**
* @type {Number}
*/
intervalId;
/**
* @type {Number}
*/
timeoutId;
/**
* @type {Number}
*/
pollingTime = HoldButton.defaultPollingTime;
/**
* @type {Boolean}
*/
holdingDown = false;
connectedCallback() {
if (Utils.onMobileDevice()) {
return;
}
this.addEventListeners();
this.setPollingTime(this.getAttribute("pollingtime"));
}
attributeChangedCallback(name, oldValue, newValue) {
switch (name) {
case "pollingtime":
this.setPollingTime(newValue);
break;
default:
break;
}
}
/**
* @param {String} newValue
*/
setPollingTime(newValue) {
this.stopPolling();
const pollingTime = parseFloat(newValue) || HoldButton.defaultPollingTime;
this.pollingTime = Utils.clamp(Math.round(pollingTime), HoldButton.minPollingTime, HoldButton.maxPollingTime);
}
addEventListeners() {
this.addEventListener("mousedown", (event) => {
if (event.button === 0) {
this.holdingDown = true;
this.startPolling();
}
}, {
passive: true
});
this.addEventListener("mouseup", (event) => {
if (event.button === 0) {
this.holdingDown = false;
this.stopPolling();
}
}, {
passive: true
});
this.addEventListener("mouseleave", () => {
if (this.holdingDown) {
this.onMouseLeaveWhileHoldingDown();
this.holdingDown = false;
}
this.stopPolling();
}, {
passive: true
});
}
startPolling() {
this.timeoutId = setTimeout(() => {
this.intervalId = setInterval(() => {
this.onmousehold();
}, this.pollingTime);
}, this.pollingTime);
}
stopPolling() {
clearTimeout(this.timeoutId);
clearInterval(this.intervalId);
}
onmousehold() {
}
onMouseLeaveWhileHoldingDown() {
}
}
class NumberComponent {
/**
* @type {HTMLInputElement}
*/
input;
/**
* @type {HoldButton}
*/
upArrow;
/**
* @type {HoldButton}
*/
downArrow;
/**
* @type {Number}
*/
increment;
/**
* @type {Boolean}
*/
get allSubComponentsConnected() {
return this.input !== null && this.upArrow !== null && this.downArrow !== null;
}
/**
* @param {HTMLDivElement} element
*/
constructor(element) {
this.connectSubElements(element);
this.initializeFields();
this.addEventListeners();
}
initializeFields() {
if (!this.allSubComponentsConnected) {
return;
}
this.increment = Utils.roundToTwoDecimalPlaces(parseFloat(this.input.getAttribute("step")) || 1);
if (this.input.onchange === null) {
this.input.onchange = () => { };
}
}
/**
* @param {HTMLDivElement} element
*/
connectSubElements(element) {
this.input = element.querySelector("input");
this.upArrow = element.querySelector(".number-arrow-up");
this.downArrow = element.querySelector(".number-arrow-down");
}
addEventListeners() {
if (!this.allSubComponentsConnected) {
return;
}
this.upArrow.onmousehold = () => {
this.incrementInput(true);
};
this.downArrow.onmousehold = () => {
this.incrementInput(false);
};
this.upArrow.addEventListener("mousedown", (event) => {
if (event.button === 0) {
this.incrementInput(true);
}
}, {
passive: true
});
this.downArrow.addEventListener("mousedown", (event) => {
if (event.button === 0) {
this.incrementInput(false);
}
}, {
passive: true
});
this.upArrow.addEventListener("mouseup", () => {
this.input.onchange();
}, {
passive: true
});
this.downArrow.addEventListener("mouseup", () => {
this.input.onchange();
}, {
passive: true
});
this.upArrow.onMouseLeaveWhileHoldingDown = () => {
this.input.onchange();
};
this.downArrow.onMouseLeaveWhileHoldingDown = () => {
this.input.onchange();
};
}
/**
* @param {Boolean} add
*/
incrementInput(add) {
const currentValue = parseFloat(this.input.value) || 1;
const incrementedValue = add ? currentValue + this.increment : currentValue - this.increment;
this.input.value = Utils.clamp(incrementedValue, 0, 9999);
}
}
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.startDebounce();
}
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 = () => { };
}
startDebounce() {
this.debouncing = true;
clearTimeout(this.timeout);
this.start();
}
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);
this.timeout = null;
}
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 (!Utils.isNumber(value)) {
return value;
}
if (this.metric === "id" && this.operator === ":") {
return value;
}
return parseInt(value);
}
}
class PostMetadata {
/**
* @type {Map.<String, PostMetadata>}
*/
static allMetadata = new Map();
static parser = new DOMParser();
/**
* @type {PostMetadata[]}
*/
static missingMetadataFetchQueue = [];
/**
* @type {PostMetadata[]}
*/
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;
static settings = {
verifyTags: true
};
/**
* @param {PostMetadata} missingMetadata
*/
static async fetchMissingMetadata(missingMetadata) {
if (missingMetadata !== undefined) {
PostMetadata.missingMetadataFetchQueue.push(missingMetadata);
}
if (PostMetadata.currentlyFetchingFromQueue) {
return;
}
PostMetadata.currentlyFetchingFromQueue = true;
while (PostMetadata.missingMetadataFetchQueue.length > 0) {
const metadata = this.missingMetadataFetchQueue.pop();
if (metadata.postIsDeleted) {
metadata.populateMetadataFromPost();
} else {
metadata.populateMetadataFromAPI(true);
}
await Utils.sleep(metadata.fetchDelay);
}
PostMetadata.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 {
Utils.addStaticInitializer(() => {
if (Utils.onFavoritesPage()) {
window.addEventListener("favoritesLoaded", () => {
PostMetadata.allFavoritesLoaded = true;
PostMetadata.missingMetadataFetchQueue = PostMetadata.missingMetadataFetchQueue.concat(PostMetadata.deletedPostFetchQueue);
PostMetadata.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;
/**
* @type {String}
*/
get apiURL() {
return Utils.getPostAPIURL(this.id);
}
/**
* @type {String}
*/
get postURL() {
return Utils.getPostPageURL(this.id);
}
/**
* @type {Number}
*/
get fetchDelay() {
return this.postIsDeleted ? PostMetadata.fetchDelay.deleted : PostMetadata.fetchDelay.normal;
}
/**
* @type {Boolean}
*/
get isEmpty() {
return this.width === 0 && this.height === 0;
}
/**
* @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 {Object.<String, String>} record
*/
populateMetadata(record) {
if (record === undefined) {
this.populateMetadataFromAPI();
} else if (record === null) {
PostMetadata.fetchMissingMetadata(this, true);
} else {
this.populateMetadataFromRecord(JSON.parse(record));
if (this.isEmpty) {
PostMetadata.fetchMissingMetadata(this, true);
}
}
}
/**
* @param {Boolean} missingInDatabase
*/
populateMetadataFromAPI(missingInDatabase = false) {
fetch(this.apiURL)
.then((response) => {
return response.text();
})
.then((html) => {
const dom = PostMetadata.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 = PostMetadata.encodeRating(metadata.getAttribute("rating"));
this.creationTimestamp = Date.parse(metadata.getAttribute("created_at"));
this.lastChangedTimestamp = parseInt(metadata.getAttribute("change"));
if (PostMetadata.settings.verifyTags) {
Post.verifyTags(this.id, metadata.getAttribute("tags"), metadata.getAttribute("file_url"));
}
const extension = Utils.getExtensionFromImageURL(metadata.getAttribute("file_url"));
if (extension !== "mp4") {
Utils.assignImageExtension(this.id, extension);
}
if (missingInDatabase) {
dispatchEvent(new CustomEvent("missingMetadata", {
detail: this.id
}));
}
})
.catch((error) => {
if (error.cause === "DeletedMetadata") {
this.postIsDeleted = true;
PostMetadata.deletedPostFetchQueue.push(this);
} else if (error.message === "Failed to fetch") {
PostMetadata.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 = PostMetadata.parser.parseFromString(html, "text/html");
const statistics = dom.getElementById("stats");
if (statistics === null) {
return;
}
const textContent = Utils.replaceLineBreaks(statistics.textContent.trim(), " ");
const match = PostMetadata.postStatisticsRegex.exec(textContent);
PostMetadata.postStatisticsRegex.lastIndex = 0;
if (!match) {
return;
}
this.width = parseInt(match[2]);
this.height = parseInt(match[3]);
this.score = parseInt(match[5]);
this.rating = PostMetadata.encodeRating(match[4]);
this.creationTimestamp = Date.parse(match[1]);
this.lastChangedTimestamp = this.creationTimestamp / 1000;
if (PostMetadata.allFavoritesLoaded) {
dispatchEvent(new CustomEvent("missingMetadata", {
detail: this.id
}));
}
})
.catch((error) => {
console.error(error);
});
}
/**
* @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 (!PostMetadata.allMetadata.has(this.id)) {
PostMetadata.allMetadata.set(this.id, this);
}
}
}
class InactivePost {
/**
* @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}`;
}
/**
* @type {String}
*/
id;
/**
* @type {String}
*/
tags;
/**
* @type {String}
*/
src;
/**
* @type {String}
*/
metadata;
/**
* @type {Boolean}
*/
fromRecord;
/**
* @param {HTMLElement | Object} favorite
*/
constructor(favorite, fromRecord) {
this.fromRecord = fromRecord;
if (fromRecord) {
this.populateAttributesFromDatabaseRecord(favorite);
} else {
this.populateAttributesFromHTMLElement(favorite);
}
}
/**
* @param {{id: String, tags: String, src: String, metadata: String}} record
*/
populateAttributesFromDatabaseRecord(record) {
this.id = record.id;
this.tags = record.tags;
this.src = InactivePost.decompressThumbSource(record.src, record.id);
this.metadata = record.metadata;
}
/**
* @param {HTMLElement} element
*/
populateAttributesFromHTMLElement(element) {
this.id = Utils.getIdFromThumb(element);
const image = Utils.getImageFromThumb(element);
this.src = image.src || image.getAttribute("data-cfsrc");
this.tags = this.preprocessTags(image);
}
/**
* @param {HTMLImageElement} image
* @returns {String}
*/
preprocessTags(image) {
const tags = Utils.correctMisspelledTags(image.title || image.getAttribute("tags"));
return Utils.removeExtraWhiteSpace(tags).split(" ").sort().join(" ");
}
instantiateMetadata() {
if (this.fromRecord) {
return new PostMetadata(this.id, this.metadata || null);
}
const favoritesMetadata = new PostMetadata(this.id);
return favoritesMetadata;
}
clear() {
this.id = null;
this.tags = null;
this.src = null;
this.metadata = null;
}
}
class Post {
/**
* @type {Map.<String, Post>}
*/
static allPosts = new Map();
/**
* @type {RegExp}
*/
static thumbSourceCompressionRegex = /thumbnails\/\/([0-9]+)\/thumbnail_([0-9a-f]+)/;
/**
* @type {HTMLElement}
*/
static template;
/**
* @type {String}
*/
static removeFavoriteButtonHTML;
/**
* @type {String}
*/
static addFavoriteButtonHTML;
static currentSortingMethod = Utils.getPreference("sortingMethod", "default");
static settings = {
deferHTMLElementCreation: true
};
static {
Utils.addStaticInitializer(() => {
if (Utils.onFavoritesPage()) {
Post.createTemplates();
Post.addEventListeners();
}
});
}
static createTemplates() {
Post.removeFavoriteButtonHTML = `<img class="remove-favorite-button add-or-remove-button" src=${Utils.createObjectURLFromSvg(Utils.icons.heartMinus)}>`;
Post.addFavoriteButtonHTML = `<img class="add-favorite-button add-or-remove-button" src=${Utils.createObjectURLFromSvg(Utils.icons.heartPlus)}>`;
const buttonHTML = Utils.userIsOnTheirOwnFavoritesPage() ? Post.removeFavoriteButtonHTML : Post.addFavoriteButtonHTML;
const canvasHTML = Utils.getPerformanceProfile() > 0 ? "" : "<canvas></canvas>";
const containerTagName = "a";
Post.template = new DOMParser().parseFromString("", "text/html").createElement("div");
Post.template.className = Utils.favoriteItemClassName;
Post.template.innerHTML = `
<${containerTagName}>
<img loading="lazy">
${buttonHTML}
${canvasHTML}
</${containerTagName}>
`;
}
static addEventListeners() {
window.addEventListener("favoriteAddedOrDeleted", (event) => {
const id = event.detail;
const post = this.allPosts.get(id);
if (post !== undefined) {
post.swapAddOrRemoveButton();
}
});
window.addEventListener("sortingParametersChanged", () => {
Post.currentSortingMethod = Utils.getSortingMethod();
const posts = Utils.getAllThumbs().map(thumb => Post.allPosts.get(thumb.id));
for (const post of posts) {
post.createMetadataHint();
}
});
}
/**
* @param {String} id
* @returns {Number}
*/
static getPixelCount(id) {
const post = Post.allPosts.get(id);
if (post === undefined || post.metadata === undefined) {
return 0;
}
return post.metadata.pixelCount;
}
/**
* @param {String} id
* @returns {String}
*/
static getExtensionFromPost(id) {
const post = Post.allPosts.get(id);
if (post === undefined) {
return undefined;
}
if (post.metadata.isEmpty()) {
return undefined;
}
return post.metadata.extension;
}
/**
* @param {String} id
* @param {String} apiTags
* @param {String} fileURL
*/
static verifyTags(id, apiTags, fileURL) {
const post = Post.allPosts.get(id);
if (post === undefined) {
return;
}
const postTagSet = new Set(post.originalTagSet);
const apiTagSet = Utils.convertToTagSet(apiTags);
if (fileURL.endsWith("mp4")) {
apiTagSet.add("video");
} else if (fileURL.endsWith("gif")) {
apiTagSet.add("gif");
} else if (!apiTagSet.has("animated_png")) {
if (apiTagSet.has("video")) {
apiTagSet.delete("video");
}
if (apiTagSet.has("animated")) {
apiTagSet.delete("animated");
}
}
postTagSet.delete(id);
if (Utils.symmetricDifference(apiTagSet, postTagSet).size > 0) {
post.initializeTags(Utils.convertToTagString(apiTagSet));
}
}
/**
* @type {Map.<String, Post>}
*/
static get postsMatchedBySearch() {
const posts = new Map();
for (const [id, post] of Post.allPosts.entries()) {
if (post.matchedByMostRecentSearch) {
posts.set(id, post);
}
}
return posts;
}
/**
* @type {String}
*/
id;
/**
* @type {HTMLDivElement}
*/
root;
/**
* @type {HTMLAnchorElement}
*/
container;
/**
* @type {HTMLImageElement}
*/
image;
/**
* @type {HTMLImageElement}
*/
addOrRemoveButton;
/**
* @type {HTMLDivElement}
*/
statisticHint;
/**
* @type {InactivePost}
*/
inactivePost;
/**
* @type {Boolean}
*/
essentialAttributesPopulated;
/**
* @type {Boolean}
*/
htmlElementCreated;
/**
* @type {Set.<String>}
*/
tagSet;
/**
* @type {Set.<String>}
*/
additionalTags;
/**
* @type {Number}
*/
originalTagsLength;
/**
* @type {Boolean}
*/
matchedByMostRecentSearch;
/**
* @type {PostMetadata}
*/
metadata;
/**
* @type {String}
*/
get href() {
return Utils.getPostPageURL(this.id);
}
/**
* @type {String}
*/
get compressedThumbSource() {
const source = this.inactivePost === null ? this.image.src : this.inactivePost.src;
return source.match(Post.thumbSourceCompressionRegex).splice(1).join("_");
}
/**
* @type {{id: String, tags: String, src: String, metadata: String}}
*/
get databaseRecord() {
return {
id: this.id,
tags: this.originalTagsString,
src: this.compressedThumbSource,
metadata: this.metadata.json
};
}
/**
* @type {Set.<String>}
*/
get originalTagSet() {
const originalTags = new Set();
let count = 0;
for (const tag of this.tagSet.values()) {
if (count >= this.originalTagsLength) {
break;
}
count += 1;
originalTags.add(tag);
}
return originalTags;
}
/**
* @type {Set.<String>}
*/
get originalTagsString() {
return Utils.convertToTagString(this.originalTagSet);
}
/**
* @type {String}
*/
get additionalTagsString() {
return Utils.convertToTagString(this.additionalTags);
}
/**
* @param {HTMLElement | Object} thumb
* @param {Boolean} fromRecord
*/
constructor(thumb, fromRecord) {
this.initializeFields();
this.initialize(new InactivePost(thumb, fromRecord));
this.setMatched(true);
this.addInstanceToAllPosts();
}
initializeFields() {
this.inactivePost = null;
this.essentialAttributesPopulated = false;
this.htmlElementCreated = false;
}
/**
* @param {InactivePost} inactivePost
*/
initialize(inactivePost) {
if (Post.settings.deferHTMLElementCreation) {
this.inactivePost = inactivePost;
this.populateEssentialAttributes(inactivePost);
} else {
this.createHTMLElement(inactivePost);
}
}
/**
* @param {InactivePost} inactivePost
*/
populateEssentialAttributes(inactivePost) {
if (this.essentialAttributesPopulated) {
return;
}
this.essentialAttributesPopulated = true;
this.id = inactivePost.id;
this.metadata = inactivePost.instantiateMetadata();
this.initializeTags(inactivePost.tags);
this.deleteConsumedProperties(inactivePost);
}
/**
* @param {InactivePost} inactivePost
*/
createHTMLElement(inactivePost) {
if (this.htmlElementCreated) {
return;
}
this.htmlElementCreated = true;
this.instantiateTemplate();
this.populateEssentialAttributes(inactivePost);
this.populateHTMLAttributes(inactivePost);
this.setupAddOrRemoveButton(Utils.userIsOnTheirOwnFavoritesPage());
this.setupClickLink();
this.deleteInactivePost();
}
activateHTMLElement() {
if (this.inactivePost !== null) {
this.createHTMLElement(this.inactivePost);
}
}
instantiateTemplate() {
this.root = Post.template.cloneNode(true);
this.container = this.root.children[0];
this.image = this.root.children[0].children[0];
this.addOrRemoveButton = this.root.children[0].children[1];
}
/**
* @param {Boolean} isRemoveButton
*/
setupAddOrRemoveButton(isRemoveButton) {
if (isRemoveButton) {
this.addOrRemoveButton.onmousedown = (event) => {
event.stopPropagation();
if (event.button === Utils.clickCodes.left) {
this.removeFavorite();
}
};
} else {
this.addOrRemoveButton.onmousedown = (event) => {
event.stopPropagation();
if (event.button === Utils.clickCodes.left) {
this.addFavorite();
}
};
}
}
removeFavorite() {
Utils.removeFavorite(this.id);
this.swapAddOrRemoveButton();
}
addFavorite() {
Utils.addFavorite(this.id);
this.swapAddOrRemoveButton();
}
swapAddOrRemoveButton() {
const isRemoveButton = this.addOrRemoveButton.classList.contains("remove-favorite-button");
this.addOrRemoveButton.outerHTML = isRemoveButton ? Post.addFavoriteButtonHTML : Post.removeFavoriteButtonHTML;
this.addOrRemoveButton = this.root.children[0].children[1];
this.setupAddOrRemoveButton(!isRemoveButton);
}
/**
* @param {InactivePost} inactivePost
*/
async populateHTMLAttributes(inactivePost) {
this.image.src = inactivePost.src;
this.image.classList.add(Utils.getContentType(inactivePost.tags || Utils.convertToTagString(this.tagSet)));
this.root.id = inactivePost.id;
if (!Utils.onMobileDevice()) {
this.container.href = await Utils.getOriginalImageURLWithExtension(this.root);
}
}
/**
* @param {String} tags
*/
initializeTags(tags) {
this.tagSet = Utils.convertToTagSet(`${this.id} ${tags}`);
this.originalTagsLength = this.tagSet.size;
this.initializeAdditionalTags();
}
initializeAdditionalTags() {
this.additionalTags = Utils.convertToTagSet(TagModifier.tagModifications.get(this.id) || "");
if (this.additionalTags.size !== 0) {
this.combineOriginalAndAdditionalTags();
}
}
/**
* @param {InactivePost} inactivePost
*/
deleteConsumedProperties(inactivePost) {
inactivePost.metadata = null;
inactivePost.tags = null;
}
setupClickLink() {
if (!Utils.onFavoritesPage()) {
return;
}
this.container.addEventListener("mousedown", (event) => {
if (event.ctrlKey) {
return;
}
const middleClick = event.button === Utils.clickCodes.middle;
const leftClick = event.button === Utils.clickCodes.left;
if (middleClick || (leftClick && !Utils.galleryEnabled())) {
Utils.openPostInNewTab(this.id);
}
});
}
deleteInactivePost() {
if (this.inactivePost !== null) {
this.inactivePost.clear();
this.inactivePost = null;
}
}
/**
* @param {HTMLElement} content
*/
insertAtEndOfContent(content) {
if (this.inactivePost !== null) {
this.createHTMLElement(this.inactivePost, true);
}
this.createMetadataHint();
content.appendChild(this.root);
}
/**
* @param {HTMLElement} content
*/
insertAtBeginningOfContent(content) {
if (this.inactivePost !== null) {
this.createHTMLElement(this.inactivePost, true);
}
this.createMetadataHint();
content.insertAdjacentElement("afterbegin", this.root);
}
addInstanceToAllPosts() {
if (!Post.allPosts.has(this.id)) {
Post.allPosts.set(this.id, this);
}
}
toggleMatched() {
this.matchedByMostRecentSearch = !this.matchedByMostRecentSearch;
}
/**
* @param {Boolean} value
*/
setMatched(value) {
this.matchedByMostRecentSearch = value;
}
combineOriginalAndAdditionalTags() {
this.tagSet = this.originalTagSet;
this.tagSet = Utils.union(this.tagSet, this.additionalTags);
}
/**
* @param {String} newTags
* @returns {String}
*/
addAdditionalTags(newTags) {
const newTagsSet = Utils.convertToTagSet(newTags);
if (newTagsSet.size > 0) {
this.additionalTags = Utils.union(this.additionalTags, newTagsSet);
this.combineOriginalAndAdditionalTags();
}
return this.additionalTagsString;
}
/**
* @param {String} tagsToRemove
* @returns {String}
*/
removeAdditionalTags(tagsToRemove) {
const tagsToRemoveSet = Utils.convertToTagSet(tagsToRemove);
if (tagsToRemoveSet.size > 0) {
this.additionalTags = Utils.difference(this.additionalTags, tagsToRemoveSet);
this.combineOriginalAndAdditionalTags();
}
return this.additionalTagsString;
}
resetAdditionalTags() {
if (this.additionalTags.size === 0) {
return;
}
this.additionalTags = new Set();
this.combineOriginalAndAdditionalTags();
}
/**
* @returns {HTMLDivElement}
*/
getMetadataHintElement() {
return this.container.querySelector(".statistic-hint");
}
/**
* @returns {Boolean}
*/
hasStatisticHint() {
return this.getMetadataHintElement() !== null;
}
/**
* @returns {String}
*/
getMetadataHintValue() {
switch (Post.currentSortingMethod) {
case "score":
return this.metadata.score;
case "width":
return this.metadata.width;
case "height":
return this.metadata.height;
case "create":
return Utils.convertTimestampToDate(this.metadata.creationTimestamp);
case "change":
return Utils.convertTimestampToDate(this.metadata.lastChangedTimestamp * 1000);
default:
return this.id;
}
}
async createMetadataHint() {
// await sleep(200);
// let hint = this.getMetadataHintElement();
// if (hint === null) {
// hint = document.createElement("div");
// hint.className = "statistic-hint";
// this.container.appendChild(hint);
// }
// hint.textContent = this.getMetadataHintValue();
}
}
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
*/
constructor(searchTag) {
this.negated = searchTag.startsWith("-");
this.value = this.negated ? searchTag.substring(1) : searchTag;
}
/**
* @param {Post} post
* @returns {Boolean}
*/
matches(post) {
if (post.tagSet.has(this.value)) {
return !this.negated;
}
return this.negated;
}
}
class WildcardSearchTag extends SearchTag {
static unmatchableRegex = /^\b$/;
static startsWithRegex = /^[^*]*\*$/;
/**
* @type {RegExp}
*/
matchRegex;
/**
* @type {Boolean}
*/
equivalentToStartsWith;
/**
* @type {String}
*/
startsWithPrefix;
/**
* @type {Number}
*/
get cost() {
return this.equivalentToStartsWith ? 10 : 20;
}
/**
* @param {String} searchTag
*/
constructor(searchTag) {
super(searchTag);
this.matchRegex = this.createWildcardRegex();
this.startsWithPrefix = this.value.slice(0, -1);
this.equivalentToStartsWith = WildcardSearchTag.startsWithRegex.test(searchTag);
this.matches = this.equivalentToStartsWith ? this.matchesPrefix : this.matchesWildcard;
}
/**
* @returns {RegExp}
*/
createWildcardRegex() {
try {
return new RegExp(`^${this.value.replaceAll(/\*/g, ".*")}$`);
} catch {
return WildcardSearchTag.unmatchableRegex;
}
}
/**
* @param {Post} post
* @returns {Boolean}
*/
matchesPrefix(post) {
for (const tag of post.tagSet.values()) {
if (tag.startsWith(this.startsWithPrefix)) {
return !this.negated;
}
if (this.startsWithPrefix < tag) {
break;
}
}
return this.negated;
}
/**
* @param {Post} post
* @returns {Boolean}
*/
matchesWildcard(post) {
for (const tag of post.tagSet.values()) {
if (this.matchRegex.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) {
super(searchTag);
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 {Post} post
* @returns {Boolean}
*/
matches(post) {
const metadata = PostMetadata.allMetadata.get(post.id);
if (metadata === undefined) {
return false;
}
if (metadata.satisfiesExpression(this.expression)) {
return !this.negated;
}
return this.negated;
}
}
class SearchCommand {
/**
* @param {String} tag
* @returns {SearchTag}
*/
static createSearchTag(tag) {
if (MetadataSearchTag.regex.test(tag)) {
return new MetadataSearchTag(tag);
}
if (tag.includes("*")) {
return new WildcardSearchTag(tag);
}
return new SearchTag(tag);
}
/**
* @param {String[]} tags
* @param {Boolean} isOrGroup
* @returns {SearchTag[]}
*/
static createSearchTagGroup(tags) {
const uniqueTags = new Set();
const searchTags = [];
for (const tag of tags) {
if (!uniqueTags.has(tag)) {
uniqueTags.add(tag);
searchTags.push(SearchCommand.createSearchTag(tag));
}
}
return searchTags;
}
/**
* @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.isEmpty = searchQuery.trim() === "";
if (this.isEmpty) {
return;
}
const {orGroups, remainingSearchTags} = Utils.extractTagGroups(searchQuery);
this.orGroups = orGroups.map(orGroup => SearchCommand.createSearchTagGroup(orGroup));
this.remainingSearchTags = SearchCommand.createSearchTagGroup(remainingSearchTags);
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 {Post} post
* @returns {Boolean}
*/
matches(post) {
if (this.isEmpty) {
return true;
}
if (!this.matchesAllRemainingSearchTags(post)) {
return false;
}
return this.matchesAllOrGroups(post);
}
/**
* @param {Post} post
* @returns {Boolean}
*/
matchesAllRemainingSearchTags(post) {
for (const searchTag of this.remainingSearchTags) {
if (!searchTag.matches(post)) {
return false;
}
}
return true;
}
/**
* @param {Post} post
* @returns {Boolean}
*/
matchesAllOrGroups(post) {
for (const orGroup of this.orGroups) {
if (!this.atLeastOnePostTagIsInOrGroup(orGroup, post)) {
return false;
}
}
return true;
}
/**
* @param {SearchTag[]} orGroup
* @param {Post} post
* @returns {Boolean}
*/
atLeastOnePostTagIsInOrGroup(orGroup, post) {
for (const orTag of orGroup) {
if (orTag.matches(post)) {
return true;
}
}
return false;
}
}
class FavoritesPageRequest {
/**
* @type {Number}
*/
pageNumber;
/**
* @type {Number}
*/
retryCount;
/**
* @type {Post[]}
*/
fetchedFavorites;
/**
* @type {String}
*/
get url() {
return `${document.location.href}&pid=${this.pageNumber * 50}`;
}
/**
* @type {Number}
*/
get retryDelay() {
return (7 ** (this.retryCount)) + 200;
}
/**
* @param {Number} pageNumber
*/
constructor(pageNumber) {
this.pageNumber = pageNumber;
this.retryCount = 0;
this.fetchedFavorites = [];
}
onFail() {
this.retryCount += 1;
}
}
class FavoritesParser {
static parser = new DOMParser();
/**
* @param {String} favoritesPageHTML
* @returns {Post[]}
*/
static extractFavorites(favoritesPageHTML) {
const elements = FavoritesParser.extractFavoriteElements(favoritesPageHTML);
return elements.map(element => new Post(element, false));
}
/**
* @param {String} favoritesPageHTML
* @returns {HTMLElement[]}
*/
static extractFavoriteElements(favoritesPageHTML) {
const dom = FavoritesParser.parser.parseFromString(favoritesPageHTML, "text/html");
const thumbs = Array.from(dom.getElementsByClassName("thumb"));
if (thumbs.length > 0) {
return thumbs;
}
return Array.from(dom.getElementsByTagName("img"))
.filter(image => image.src.includes("thumbnail_"))
.map(image => image.parentElement);
}
}
class FetchedFavoritesQueue {
/**
* @type {FavoritesPageRequest[]}
*/
queue;
/**
* @type {Function}
*/
onDequeue;
/**
* @type {Number}
*/
lastDequeuedPageNumber;
/**
* @type {Boolean}
*/
dequeuing;
/**
* @type {Number}
*/
get lowestEnqueuedPageNumber() {
return this.queue[0].pageNumber;
}
/**
* @type {Number}
*/
get nextPageNumberToDequeue() {
return this.lastDequeuedPageNumber + 1;
}
/**
* @type {Boolean}
*/
get allPreviousPagesWereDequeued() {
return this.nextPageNumberToDequeue === this.lowestEnqueuedPageNumber;
}
/**
* @type {Boolean}
*/
get isEmpty() {
return this.queue.length === 0;
}
/**
* @type {Boolean}
*/
get canDequeue() {
return !this.isEmpty && this.allPreviousPagesWereDequeued;
}
/**
* @param {Function}
*/
constructor(onDequeue) {
this.onDequeue = onDequeue;
this.lastDequeuedPageNumber = -1;
this.queue = [];
}
/**
* @param {FavoritesPageRequest} request
*/
enqueue(request) {
this.queue.push(request);
this.sortByLowestPageNumber();
this.dequeueAll();
}
sortByLowestPageNumber() {
this.queue.sort((request1, request2) => request1.pageNumber - request2.pageNumber);
}
dequeueAll() {
if (this.dequeuing) {
return;
}
this.dequeuing = true;
while (this.canDequeue) {
this.dequeue();
}
this.dequeuing = false;
}
dequeue() {
this.lastDequeuedPageNumber += 1;
this.onDequeue(this.queue.shift());
}
}
class FavoritesFetcher {
/**
* @type {Function}
*/
onAllFavoritesPageRequestsCompleted;
/**
* @type {Function}
*/
onFavoritesPageRequestCompleted;
/**
* @type {FavoritesPageRequest[]}
*/
failedFavoritesPageRequests;
/**
* @type {Set.<String>}
*/
storedFavoriteIds;
/**
* @type {Number}
*/
currentFavoritesPageNumber;
/**
* @type {Boolean}
*/
fetchedAnEmptyFavoritesPage;
/**
* @type {Boolean}
*/
get hasFailedFavoritesPageRequests() {
return this.failedFavoritesPageRequests.length > 0;
}
/**
* @type {Boolean}
*/
get hasNotFetchedAllFavoritesPages() {
return !this.fetchedAnEmptyFavoritesPage;
}
/**
* @type {Boolean}
*/
get allFavoritesPageRequestsHaveNotCompleted() {
return this.hasNotFetchedAllFavoritesPages || this.hasFailedFavoritesPageRequests;
}
/**
* @type {FavoritesPageRequest}
*/
get oldestFailedFavoritesPageFetchRequest() {
return this.failedFavoritesPageRequests.shift();
}
/**
* @type {FavoritesPageRequest}
*/
get newFavoritesPageFetchRequest() {
const request = new FavoritesPageRequest(this.currentFavoritesPageNumber);
this.currentFavoritesPageNumber += 1;
return request;
}
/**
* @type {FavoritesPageRequest | null}
*/
get currentFetchRequest() {
if (this.hasFailedFavoritesPageRequests) {
return this.oldestFailedFavoritesPageFetchRequest;
}
if (this.hasNotFetchedAllFavoritesPages) {
return this.newFavoritesPageFetchRequest;
}
return null;
}
/**
* @param {Function} onAllFavoritesPageRequestsCompleted
* @param {Function} onFavoritesPageRequestCompleted
*/
constructor(onAllFavoritesPageRequestsCompleted, onFavoritesPageRequestCompleted) {
this.onAllFavoritesPageRequestsCompleted = onAllFavoritesPageRequestsCompleted;
this.onFavoritesPageRequestCompleted = onFavoritesPageRequestCompleted;
this.storedFavoriteIds = new Set();
this.failedFavoritesPageRequests = [];
this.currentFavoritesPageNumber = 0;
this.fetchedAnEmptyFavoritesPage = false;
}
async fetchAllFavorites() {
while (this.allFavoritesPageRequestsHaveNotCompleted) {
await this.fetchFavoritesPage(this.currentFetchRequest);
}
this.onAllFavoritesPageRequestsCompleted();
}
/**
* @param {Set.<String>} storedFavoriteIds
*/
async fetchAllNewFavoritesOnReload(storedFavoriteIds) {
this.storedFavoriteIds = storedFavoriteIds;
let favorites = [];
while (true) {
const {allNewFavoritesFound, newFavorites} = await this.fetchNewFavoritesOnReload();
favorites = favorites.concat(newFavorites);
if (allNewFavoritesFound) {
this.storedFavoriteIds = null;
this.onAllFavoritesPageRequestsCompleted(favorites);
return;
}
}
}
/**
* @returns {Promise.<{allNewFavoritesFound: Boolean, newFavorites: Post[]}>}
*/
fetchNewFavoritesOnReload() {
return fetch(this.newFavoritesPageFetchRequest.url)
.then((response) => {
return response.text();
})
.then((html) => {
return this.extractNewFavorites(html);
});
}
/**
* @param {String} html
* @returns {{allNewFavoritesFound: Boolean, newFavorites: Post[]}}
*/
extractNewFavorites(html) {
const newFavorites = [];
const fetchedFavorites = FavoritesParser.extractFavorites(html);
let allNewFavoritesFound = fetchedFavorites.length === 0;
for (const favorite of fetchedFavorites) {
if (this.storedFavoriteIds.has(favorite.id)) {
allNewFavoritesFound = true;
break;
}
newFavorites.push(favorite);
}
return {
allNewFavoritesFound,
newFavorites
};
}
/**
* @param {FavoritesPageRequest} request
*/
async fetchFavoritesPage(request) {
if (request === null) {
console.error("Error: null fetch request");
return;
}
fetch(request.url)
.then((response) => {
return this.onFavoritesPageRequestResponse(response);
})
.then((html) => {
this.onFavoritesPageRequestSuccess(request, html);
})
.catch((error) => {
this.onFavoritesPageRequestFail(request, error);
});
await Utils.sleep(request.retryDelay);
}
/**
* @param {Response} response
* @returns {Promise.<String>}
*/
onFavoritesPageRequestResponse(response) {
if (response.ok) {
return response.text();
}
throw new Error(`${response.status}: Failed to fetch, ${response.url}`);
}
/**
* @param {FavoritesPageRequest} request
* @param {String} html
*/
onFavoritesPageRequestSuccess(request, html) {
request.fetchedFavorites = FavoritesParser.extractFavorites(html);
const favoritesPageIsEmpty = request.fetchedFavorites.length === 0;
this.fetchedAnEmptyFavoritesPage = this.fetchedAnEmptyFavoritesPage || favoritesPageIsEmpty;
if (!favoritesPageIsEmpty) {
this.onFavoritesPageRequestCompleted(request);
}
}
/**
* @param {FavoritesPageRequest} request
* @param {Error} error
*/
onFavoritesPageRequestFail(request, error) {
console.error(error);
request.onFail();
this.failedFavoritesPageRequests.push(request);
}
}
class FavoritesPaginator {
/**
* @type {HTMLDivElement}
*/
content;
/**
* @type {HTMLElement}
*/
paginationMenu;
/**
* @type {HTMLLabelElement}
*/
paginationLabel;
/**
* @type {Number}
*/
currentPageNumber;
/**
* @type {Number}
*/
maxFavoritesPerPage;
/**
* @type {Number}
*/
maxPageNumberButtons;
constructor() {
this.content = this.createContentContainer();
this.paginationMenu = this.createPaginationMenuContainer();
this.currentPageNumber = 1;
this.favoritesPerPage = Utils.getPreference("resultsPerPage", Utils.defaults.resultsPerPage);
this.maxPageNumberButtons = Utils.onMobileDevice() ? 3 : 5;
}
/**
* @returns {HTMLDivElement}
*/
createContentContainer() {
const content = document.createElement("div");
content.id = "favorites-search-gallery-content";
Utils.favoritesSearchGalleryContainer.appendChild(content);
return content;
}
/**
* @returns {HTMLDivElement}
*/
createPaginationMenuContainer() {
const container = document.createElement("span");
container.id = "favorites-pagination-container";
return container;
}
insertPaginationMenuContainer() {
if (document.getElementById(this.paginationMenu.id) === null) {
if (Utils.onMobileDevice()) {
document.getElementById("favorites-search-gallery-menu").insertAdjacentElement("afterbegin", this.paginationMenu);
} else {
const placeToInsertPagination = document.getElementById("favorites-pagination-placeholder");
placeToInsertPagination.insertAdjacentElement("afterend", this.paginationMenu);
placeToInsertPagination.remove();
}
}
}
/**
* @param {Post[]} favorites
*/
paginate(favorites) {
this.insertPaginationMenuContainer();
this.changePage(1, favorites);
}
/**
* @param {Post[]} favorites
*/
paginateWhileFetching(favorites) {
const pageNumberButtons = document.getElementsByClassName("pagination-number");
const lastPageButtonNumber = pageNumberButtons.length > 0 ? parseInt(pageNumberButtons[pageNumberButtons.length - 1].textContent) : 1;
const pageCount = this.getPageCount(favorites.length);
const needsToCreateNewPage = pageCount > lastPageButtonNumber;
const nextPageButton = document.getElementById("next-page");
const alreadyAtMaxPageNumberButtons = document.getElementsByClassName("pagination-number").length >= this.maxPageNumberButtons &&
nextPageButton !== null && nextPageButton.style.display !== "none" &&
nextPageButton.style.visibility !== "hidden";
if (needsToCreateNewPage && !alreadyAtMaxPageNumberButtons) {
this.createPaginationMenu(this.currentPageNumber, favorites);
} else {
this.updateTraversalButtonEventListeners(favorites);
this.updatePageNumberButtonEventListeners(favorites);
}
const onLastPage = (pageCount === this.currentPageNumber);
if (!onLastPage) {
return;
}
const range = this.getPaginationRange(this.currentPageNumber);
const favoritesToAdd = favorites.slice(range.start, range.end)
.filter(favorite => document.getElementById(favorite.id) === null);
for (const favorite of favoritesToAdd) {
favorite.insertAtEndOfContent(this.content);
}
this.setPaginationLabel(this.currentPageNumber, favorites.length);
}
/**
* @param {Number} pageNumber
* @param {Post[]} favorites
*/
changePage(pageNumber, favorites) {
this.currentPageNumber = pageNumber;
this.createPaginationMenu(pageNumber, favorites);
this.showFavorites(pageNumber, favorites);
if (FavoritesLoader.currentState !== FavoritesLoader.states.loadingFavoritesFromDatabase) {
dispatchEvent(new Event("changedPage"));
}
}
/**
* @param {Number} pageNumber
* @param {Post[]} favorites
*/
createPaginationMenu(pageNumber, favorites) {
this.paginationMenu.innerHTML = "";
this.setPaginationLabel(pageNumber, favorites.length);
this.createPageNumberButtons(pageNumber, favorites);
this.createPageTraversalButtons(favorites);
this.createGotoSpecificPageInputs(favorites);
}
/**
* @param {Number} pageNumber
* @param {Number} favoriteCount
*/
setPaginationLabel(pageNumber, favoriteCount) {
const range = this.getPaginationRange(pageNumber);
const start = range.start;
const end = Math.min(range.end, favoriteCount);
if (this.paginationLabel === undefined) {
this.paginationLabel = document.getElementById("pagination-label");
}
if (favoriteCount <= this.maxFavoritesPerPage || isNaN(start) || isNaN(end)) {
this.paginationLabel.textContent = "";
return;
}
this.paginationLabel.textContent = `${start + 1} - ${end}`;
}
/**
* @param {Number} pageNumber
* @returns {{start: Number, end: Number}}
*/
getPaginationRange(pageNumber) {
return {
start: this.maxFavoritesPerPage * (pageNumber - 1),
end: this.maxFavoritesPerPage * pageNumber
};
}
/**
* @param {Number} favoriteCount
* @returns {Number}
*/
getPageCount(favoriteCount) {
if (favoriteCount === 0) {
return 1;
}
const pageCount = favoriteCount / this.maxFavoritesPerPage;
if (favoriteCount % this.maxFavoritesPerPage === 0) {
return pageCount;
}
return Math.floor(pageCount) + 1;
}
/**
* @param {Number} pageNumber
* @param {Post[]} favorites
*/
createPageNumberButtons(pageNumber, favorites) {
const pageCount = this.getPageCount(favorites.length);
let numberOfButtonsCreated = 0;
for (let i = pageNumber; i <= pageCount && numberOfButtonsCreated < this.maxPageNumberButtons; i += 1) {
numberOfButtonsCreated += 1;
this.createPageNumberButton(pageNumber, i, favorites);
}
if (numberOfButtonsCreated >= this.maxPageNumberButtons) {
return;
}
for (let j = pageNumber - 1; j >= 1 && numberOfButtonsCreated < this.maxPageNumberButtons; j -= 1) {
numberOfButtonsCreated += 1;
this.createPageNumberButton(pageNumber, j, favorites, "afterbegin");
}
}
/**
* @param {Number} currentPageNumber
* @param {Number} pageNumber
* @param {Post[]} favorites
* @param {InsertPosition} position
*/
createPageNumberButton(currentPageNumber, pageNumber, favorites, 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.changePage(pageNumber, favorites);
};
this.paginationMenu.insertAdjacentElement(position, pageNumberButton);
pageNumberButton.textContent = pageNumber;
}
/**
* @param {Post[]} favorites
*/
updatePageNumberButtonEventListeners(favorites) {
const pageNumberButtons = document.getElementsByClassName("pagination-number");
for (const pageNumberButton of pageNumberButtons) {
const pageNumber = parseInt(Utils.removeNonNumericCharacters(pageNumberButton.id));
pageNumberButton.onclick = () => {
this.changePage(pageNumber, favorites);
};
}
}
/**
* @param {Post[]} favorites
*/
createPageTraversalButtons(favorites) {
const pageCount = this.getPageCount(favorites.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.title = "Goto previous page";
firstPage.title = "Goto first page";
nextPage.title = "Goto next page";
finalPage.title = "Goto last page";
previousPage.onclick = () => {
if (this.currentPageNumber - 1 >= 1) {
this.changePage(this.currentPageNumber - 1, favorites);
}
};
firstPage.onclick = () => {
this.changePage(1, favorites);
};
nextPage.onclick = () => {
if (this.currentPageNumber + 1 <= pageCount) {
this.changePage(this.currentPageNumber + 1, favorites);
}
};
finalPage.onclick = () => {
this.changePage(pageCount, favorites);
};
this.paginationMenu.insertAdjacentElement("afterbegin", previousPage);
this.paginationMenu.insertAdjacentElement("afterbegin", firstPage);
this.paginationMenu.appendChild(nextPage);
this.paginationMenu.appendChild(finalPage);
this.updateVisibilityOfPageTraversalButtons(previousPage, firstPage, nextPage, finalPage, this.getPageCount(favorites.length));
}
/**
* @param {Post[]} favorites
*/
createGotoSpecificPageInputs(favorites) {
if (this.firstPageNumberButtonExists() && this.lastPageNumberButtonExists(this.getPageCount(favorites.length))) {
return;
}
const html = `
<input type="number" placeholder="page" style="width: 4em;" id="goto-page-input">
<button id="goto-page-button">Go</button>
`;
const container = document.createElement("span");
container.title = "Goto specific page";
container.innerHTML = html;
const input = container.children[0];
const button = container.children[1];
input.onkeydown = (event) => {
if (event.key === "Enter") {
button.click();
}
};
this.paginationMenu.appendChild(container);
this.updateTraversalButtonEventListeners(favorites);
}
/**
* @param {Post[]} favorites
*/
updateTraversalButtonEventListeners(favorites) {
const gotoPageButton = document.getElementById("goto-page-button");
const finalPageButton = document.getElementById("final-page");
const input = document.getElementById("goto-page-input");
const pageCount = this.getPageCount(favorites.length);
if (gotoPageButton === null || finalPageButton === null || input === null) {
return;
}
gotoPageButton.onclick = () => {
let pageNumber = parseInt(input.value);
if (!Utils.isNumber(pageNumber)) {
return;
}
pageNumber = Utils.clamp(pageNumber, 1, pageCount);
this.changePage(pageNumber, favorites);
};
finalPageButton.onclick = () => {
this.changePage(pageCount, favorites);
};
}
/**
* @param {Number} pageNumber
* @param {Post[]} favorites
*/
showFavorites(pageNumber, favorites) {
const {start, end} = this.getPaginationRange(pageNumber);
const newContent = document.createDocumentFragment();
for (const favorite of favorites.slice(start, end)) {
favorite.insertAtEndOfContent(newContent);
}
this.content.innerHTML = "";
this.content.appendChild(newContent);
window.scrollTo(0, 0);
}
/**
* @returns {Boolean}
*/
firstPageNumberButtonExists() {
return document.getElementById("favorites-page-1") !== null;
}
/**
* @param {Number} pageCount
* @returns {Boolean}
*/
lastPageNumberButtonExists(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.firstPageNumberButtonExists();
const lastNumberExists = this.lastPageNumberButtonExists(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 {String} direction
* @param {Post[]} favorites
*/
changePageWhileInGallery(direction, favorites) {
const pageCount = this.getPageCount(favorites.length);
const onLastPage = this.currentPageNumber === pageCount;
const onFirstPage = this.currentPageNumber === 1;
const onlyOnePage = onFirstPage && onLastPage;
if (onlyOnePage) {
dispatchEvent(new CustomEvent("didNotChangePageInGallery", {
detail: direction
}));
return;
}
if (onLastPage && direction === "ArrowRight") {
this.changePage(1, favorites);
return;
}
if (onFirstPage && direction === "ArrowLeft") {
this.changePage(pageCount, favorites);
return;
}
const newPageNumber = direction === "ArrowRight" ? this.currentPageNumber + 1 : this.currentPageNumber - 1;
this.changePage(newPageNumber, favorites);
}
/**
* @param {Boolean} value
*/
toggleContentVisibility(value) {
this.content.style.display = value ? "" : "none";
}
/**
* @param {Post} favorite
*/
insertNewFavorite(favorite) {
favorite.insertAtBeginningOfContent(this.content);
}
/**
* @param {Number} id
*/
findFavorite(id) {
// const favorites = this.latestSearchResults;
// const favoriteIds = favorites.map(favorite => favorite.id);
// const index = favoriteIds.indexOf(id);
// if (index === -1) {
// return;
// }
// const pageNumber = Math.floor(index / this.favoritesPerPage) + 1;
// dispatchEvent(new CustomEvent("foundFavorite", {
// detail: id
// }));
// this.changePage(pageNumber, favorites);
// setTimeout(() => {
// scrollToThumb(id, true, false);
// }, 600);
}
}
class FavoritesSearchFlags {
/**
* @type {Boolean}
*/
searchResultsAreShuffled;
/**
* @type {Boolean}
*/
searchResultsAreInverted;
/**
* @type {Boolean}
*/
searchResultsWereShuffled;
/**
* @type {Boolean}
*/
searchResultsWereInverted;
/**
* @type {Boolean}
*/
recentlyChangedResultsPerPage;
/**
* @type {Boolean}
*/
tagsWereModified;
/**
* @type {Boolean}
*/
excludeBlacklistWasClicked;
/**
* @type {Boolean}
*/
sortingParametersWereChanged;
/**
* @type {Boolean}
*/
allowedRatingsWereChanged;
/**
* @type {String}
*/
searchQuery;
/**
* @type {String}
*/
previousSearchQuery;
/**
* @type {Boolean}
*/
get onFirstPage() {
const firstPageNumberButton = document.getElementById("favorites-page-1");
return firstPageNumberButton !== null && firstPageNumberButton.classList.contains("selected");
}
/**
* @type {Boolean}
*/
get notOnFirstPage() {
return !this.onFirstPage;
}
/**
* @type {Boolean}
*/
get aNewSearchCouldProduceDifferentResults() {
return this.searchQuery !== this.previousSearchQuery ||
FavoritesLoader.currentState !== FavoritesLoader.states.allFavoritesLoaded ||
this.searchResultsAreShuffled ||
this.searchResultsAreInverted ||
this.searchResultsWereShuffled ||
this.searchResultsWereInverted ||
this.recentlyChangedResultsPerPage ||
this.tagsWereModified ||
this.excludeBlacklistWasClicked ||
this.sortingParametersWereChanged ||
this.allowedRatingsWereChanged ||
this.notOnFirstPage;
}
constructor() {
this.searchResultsAreShuffled = false;
this.searchResultsAreInverted = false;
this.searchResultsWereShuffled = false;
this.searchResultsWereInverted = false;
this.recentlyChangedResultsPerPage = false;
this.tagsWereModified = false;
this.excludeBlacklistWasClicked = false;
this.sortingParametersWereChanged = false;
this.allowedRatingsWereChanged = false;
this.searchQuery = "";
this.previousSearchQuery = "";
}
resetFlagsImplyingDifferentSearchResults() {
this.searchResultsWereShuffled = this.searchResultsAreShuffled;
this.searchResultsWereInverted = this.searchResultsAreInverted;
this.tagsWereModified = false;
this.excludeBlacklistWasClicked = false;
this.sortingParametersWereChanged = false;
this.allowedRatingsWereChanged = false;
this.searchResultsAreShuffled = false;
this.searchResultsAreInverted = false;
this.recentlyChangedResultsPerPage = false;
this.previousSearchQuery = this.searchQuery;
}
}
class FavoritesDatabaseWrapper {
static databaseName = "Favorites";
static objectStoreName = `user${Utils.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;
}
createObjectStore() {
return 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 = {};
let database;
await this.openConnection()
.then(async(connectionEvent) => {
/**
* @type {IDBDatabase}
*/
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);
};
}).catch(async(error) => {
this.version += 1;
if (error.name === "NotFoundError") {
database.close();
await this.createObjectStore();
}
this.loadFavorites(idsToDelete);
});
}
/**
* @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;
}
};
`
};
/**
* @type {Function}
*/
onFavoritesStored;
/**
* @type {Function}
*/
onFavoritesLoaded;
/**
* @type {Worker}
*/
databaseWorker;
/**
* @type {String[]}
*/
favoriteIdsRequiringMetadataDatabaseUpdate;
/**
* @type {Number}
*/
newMetadataReceivedTimeout;
/**
* @param {Function} onFavoritesStored
* @param {Function} onFavoritesLoaded
*/
constructor(onFavoritesStored, onFavoritesLoaded) {
this.onFavoritesStored = onFavoritesStored;
this.onFavoritesLoaded = onFavoritesLoaded;
this.favoriteIdsRequiringMetadataDatabaseUpdate = [];
this.addEventListeners();
this.initializeDatabase();
}
addEventListeners() {
window.addEventListener("missingMetadata", (event) => {
this.addNewMetadata(event.detail);
});
}
initializeDatabase() {
this.databaseWorker = new Worker(Utils.getWorkerURL(FavoritesDatabaseWrapper.webWorkers.database));
this.databaseWorker.onmessage = (message) => {
switch (message.data.response) {
case "finishedLoading":
this.onFavoritesLoaded(message.data.favorites);
break;
case "finishedStoring":
this.onFavoritesStored();
break;
default:
break;
}
};
this.databaseWorker.postMessage({
command: "create",
objectStoreName: FavoritesDatabaseWrapper.objectStoreName,
version: 1
});
}
/**
* @returns {String[]}
*/
getIdsToDeleteOnReload() {
if (Utils.userIsOnTheirOwnFavoritesPage()) {
const idsToDelete = Utils.getIdsToDeleteOnReload();
Utils.clearIdsToDeleteOnReload();
return idsToDelete;
}
return [];
}
/**
* @param {Post[]} favorites
*/
storeAllFavorites(favorites) {
this.storeFavorites(favorites.slice().reverse());
}
/**
* @param {Post[]} favorites
*/
async storeFavorites(favorites) {
await Utils.sleep(500);
this.databaseWorker.postMessage({
command: "store",
favorites: favorites.map(post => post.databaseRecord)
});
}
loadAllFavorites() {
this.databaseWorker.postMessage({
command: "load",
idsToDelete: this.getIdsToDeleteOnReload()
});
}
/**
* @param {String} postId
*/
addNewMetadata(postId) {
if (!Post.allPosts.has(postId)) {
return;
}
const batchSize = 500;
const waitTime = 1000;
clearTimeout(this.newMetadataReceivedTimeout);
this.favoriteIdsRequiringMetadataDatabaseUpdate.push(postId);
if (this.favoriteIdsRequiringMetadataDatabaseUpdate.length >= batchSize) {
this.updateMetadataInDatabase();
return;
}
this.newMetadataReceivedTimeout = setTimeout(() => {
this.updateMetadataInDatabase();
}, waitTime);
}
updateMetadataInDatabase() {
this.updateFavorites(this.favoriteIdsRequiringMetadataDatabaseUpdate.map(id => Post.allPosts.get(id)));
this.favoriteIdsRequiringMetadataDatabaseUpdate = [];
}
/**
* @param {Post[]} posts
*/
updateFavorites(posts) {
this.databaseWorker.postMessage({
command: "update",
favorites: posts.map(post => post.databaseRecord)
});
}
}
class FavoritesLoader {
static states = {
initial: 0,
fetchingFavorites: 1,
loadingFavoritesFromDatabase: 2,
allFavoritesLoaded: 3
};
static currentState = FavoritesLoader.states.initial;
static tagNegation = {
useTagBlacklist: true,
negatedTagBlacklist: Utils.negateTags(Utils.tagBlacklist)
};
static get disabled() {
return !Utils.onFavoritesPage();
}
/**
* @type {Post[]}
*/
allFavorites;
/**
* @type {Post[]}
*/
latestSearchResults;
/**
* @type {HTMLLabelElement}
*/
matchCountLabel;
/**
* @type {Number}
*/
searchResultCount;
/**
* @type {Number | null}
*/
expectedTotalFavoritesCount;
/**
* @type {String}
*/
searchQuery;
/**
* @type {Post[]}
*/
searchResultsWhileFetching;
/**
* @type {Number}
*/
allowedRatings;
/**
* @type {FavoritesFetcher}
*/
fetcher;
/**
* @type {FetchedFavoritesQueue}
*/
fetchedQueue;
/**
* @type {FavoritesPaginator}
*/
paginator;
/**
* @type {FavoritesSearchFlags}
*/
searchFlags;
/**
* @type {FavoritesDatabaseWrapper}
*/
database;
/**
* @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;
}
/**
* @type {Set.<String>}
*/
get allFavoriteIds() {
return new Set(Array.from(this.allFavorites.values()).map(post => post.id));
}
/**
* @type {Post[]}
*/
get getFavoritesMatchedByLastSearch() {
return this.allFavorites.filter(post => post.matchedByMostRecentSearch);
}
constructor() {
if (FavoritesLoader.disabled) {
return;
}
this.initializeFields();
this.initializeComponents();
this.addEventListeners();
this.setExpectedFavoritesCount();
Utils.clearOriginalFavoritesPage();
this.searchFavorites();
}
initializeFields() {
this.allFavorites = [];
this.latestSearchResults = [];
this.searchResultsWhileFetching = [];
this.matchCountLabel = document.getElementById("match-count-label");
this.allowedRatings = Utils.loadAllowedRatings();
this.expectedTotalFavoritesCount = null;
this.searchResultCount = 0;
this.searchQuery = "";
}
initializeComponents() {
this.fetchedQueue = new FetchedFavoritesQueue((request) => {
this.processFetchedFavorites(request.fetchedFavorites);
});
this.fetcher = new FavoritesFetcher(() => {
this.onAllFavoritesFetched();
}, (request) => {
this.fetchedQueue.enqueue(request);
});
this.paginator = new FavoritesPaginator();
this.searchFlags = new FavoritesSearchFlags();
this.database = new FavoritesDatabaseWrapper(() => {
this.onFavoritesStoredToDatabase();
}, (favorites) => {
this.onAllFavoritesLoadedFromDatabase(favorites);
});
}
addEventListeners() {
window.addEventListener("modifiedTags", () => {
this.searchFlags.tagsWereModified = true;
});
window.addEventListener("reachedEndOfGallery", (event) => {
this.paginator.changePageWhileInGallery(event.detail, this.latestSearchResults);
});
}
setExpectedFavoritesCount() {
const profileURL = `https://rule34.xxx/index.php?page=account&s=profile&id=${Utils.getFavoritesPageId()}`;
fetch(profileURL)
.then((response) => {
if (response.ok) {
return response.text();
}
throw new Error(response.status);
})
.then((html) => {
const table = new DOMParser().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.expectedTotalFavoritesCount = favoritesCount;
})
.catch(() => {
console.error(`Could not find total favorites count from ${profileURL}, are you logged in?`);
});
}
/**
* @param {String} searchQuery
*/
searchFavorites(searchQuery) {
this.setSearchQuery(searchQuery);
dispatchEvent(new Event("searchStarted"));
this.showSearchResults();
}
/**
* @param {String} searchQuery
*/
setSearchQuery(searchQuery) {
if (searchQuery !== undefined) {
this.searchQuery = searchQuery;
this.searchFlags.searchQuery = searchQuery;
}
}
showSearchResults() {
switch (FavoritesLoader.currentState) {
case FavoritesLoader.states.initial:
this.loadAllFavoritesFromDatabase();
break;
case FavoritesLoader.states.fetchingFavorites:
this.showSearchResultsWhileFetchingFavorites();
break;
case FavoritesLoader.states.loadingFavoritesFromDatabase:
break;
case FavoritesLoader.states.allFavoritesLoaded:
this.showSearchResultsAfterAllFavoritesLoaded();
break;
default:
console.error(`Invalid FavoritesLoader state: ${FavoritesLoader.currentState}`);
break;
}
}
showSearchResultsWhileFetchingFavorites() {
this.searchResultsWhileFetching = this.getSearchResults(this.allFavorites);
this.paginateSearchResults(this.searchResultsWhileFetching);
}
showSearchResultsAfterAllFavoritesLoaded() {
this.paginateSearchResults(this.getSearchResults(this.allFavorites));
}
/**
* @param {Post[]} posts
* @returns {Post[]}
*/
getSearchResults(posts) {
const searchCommand = new SearchCommand(this.finalSearchQuery);
const results = [];
for (const post of posts) {
if (searchCommand.matches(post)) {
results.push(post);
post.setMatched(true);
} else {
post.setMatched(false);
}
}
return results;
}
fetchNewFavoritesOnReload() {
this.fetcher.onAllFavoritesPageRequestsCompleted = (newFavorites) => {
this.addNewFavoritesOnReload(newFavorites);
};
this.fetcher.fetchAllNewFavoritesOnReload(this.allFavoriteIds);
}
/**
* @param {Post[]} newFavorites
*/
addNewFavoritesOnReload(newFavorites) {
this.allFavorites = newFavorites.concat(this.allFavorites);
this.latestSearchResults = newFavorites.concat(this.latestSearchResults);
if (newFavorites.length === 0) {
dispatchEvent(new CustomEvent("newFavoritesFetchedOnReload", {
detail: {
empty: true,
thumbs: []
}
}));
this.toggleStatusText(false);
return;
}
this.setStatusText(`Found ${newFavorites.length} new favorite${newFavorites.length === 1 ? "" : "s"}`);
this.toggleStatusText(false, 1000);
this.database.storeFavorites(newFavorites);
this.insertNewFavorites(newFavorites);
}
fetchAllFavorites() {
FavoritesLoader.currentState = FavoritesLoader.states.fetchingFavorites;
this.paginator.toggleContentVisibility(true);
this.paginator.insertPaginationMenuContainer();
this.paginator.createPaginationMenu(1, []);
this.fetcher.fetchAllFavorites();
dispatchEvent(new Event("readyToSearch"));
setTimeout(() => {
dispatchEvent(new Event("startedFetchingFavorites"));
}, 50);
}
updateStatusWhileFetching() {
let statusText = `Fetching Favorites ${this.allFavorites.length}`;
if (this.expectedTotalFavoritesCount !== null) {
statusText = `${statusText} / ${this.expectedTotalFavoritesCount}`;
}
this.setStatusText(statusText);
}
/**
* @param {Post[]} favorites
*/
processFetchedFavorites(favorites) {
const matchedFavorites = this.getSearchResults(favorites);
this.searchResultsWhileFetching = this.searchResultsWhileFetching.concat(matchedFavorites);
const searchResultsWhileFetchingWithAllowedRatings = this.getResultsWithAllowedRatings(this.searchResultsWhileFetching);
this.updateMatchCount(searchResultsWhileFetchingWithAllowedRatings.length);
this.allFavorites = this.allFavorites.concat(favorites);
this.addFetchedFavoritesToContent(searchResultsWhileFetchingWithAllowedRatings);
this.updateStatusWhileFetching();
dispatchEvent(new CustomEvent("favoritesFetched", {
detail: favorites.map(post => post.root)
}));
}
invertSearchResults() {
this.resetMatchCount();
this.allFavorites.forEach((post) => {
post.toggleMatched();
});
const invertedSearchResults = this.getFavoritesMatchedByLastSearch;
this.searchFlags.searchResultsAreInverted = true;
this.paginateSearchResults(invertedSearchResults);
window.scrollTo(0, 0);
}
shuffleSearchResults() {
const matchedPosts = this.getFavoritesMatchedByLastSearch;
Utils.shuffleArray(matchedPosts);
this.searchFlags.searchResultsAreShuffled = true;
this.paginateSearchResults(matchedPosts);
}
onAllFavoritesFetched() {
this.latestSearchResults = this.getResultsWithAllowedRatings(this.searchResultsWhileFetching);
dispatchEvent(new CustomEvent("newSearchResults", {
detail: this.latestSearchResults
}));
this.onAllFavoritesLoaded();
this.database.storeAllFavorites(this.allFavorites);
this.setStatusText("Saving favorites");
}
/**
* @param {Object[]} records
*/
onAllFavoritesLoadedFromDatabase(records) {
this.toggleLoadingUI(false);
if (records.length === 0) {
this.fetchAllFavorites();
return;
}
this.setStatusText("All favorites loaded");
this.paginateSearchResults(this.deserializeFavorites(records));
this.onAllFavoritesLoaded();
setTimeout(() => {
this.fetchNewFavoritesOnReload();
}, 100);
}
onFavoritesStoredToDatabase() {
this.setStatusText("All favorites saved");
this.toggleStatusText(false, 1000);
}
onAllFavoritesLoaded() {
dispatchEvent(new Event("readyToSearch"));
dispatchEvent(new Event("favoritesLoaded"));
FavoritesLoader.currentState = FavoritesLoader.states.allFavoritesLoaded;
}
/**
* @param {Boolean} value
*/
toggleLoadingUI(value) {
this.showLoadingWheel(value);
this.paginator.toggleContentVisibility(!value);
}
/**
* @param {Object[]} records
* @returns {Post[]}}
*/
deserializeFavorites(records) {
const searchCommand = new SearchCommand(this.finalSearchQuery);
const searchResults = [];
for (const record of records) {
const post = new Post(record, true);
const isBlacklisted = !searchCommand.matches(post);
if (isBlacklisted) {
if (!Utils.userIsOnTheirOwnFavoritesPage()) {
continue;
}
post.setMatched(false);
} else {
searchResults.push(post);
}
this.allFavorites.push(post);
}
return searchResults;
}
loadAllFavoritesFromDatabase() {
FavoritesLoader.currentState = FavoritesLoader.states.loadingFavoritesFromDatabase;
this.toggleLoadingUI(true);
this.setStatusText("Loading favorites");
this.database.loadAllFavorites();
}
/**
* @param {Boolean} value
*/
showLoadingWheel(value) {
document.getElementById("loading-wheel").style.display = value ? "flex" : "none";
}
/**
* @param {Boolean} value
* @param {Number} delay
*/
async toggleStatusText(value, delay) {
if (delay !== undefined && delay > 0) {
await Utils.sleep(delay);
}
document.getElementById("favorites-load-status-label").style.display = value ? "inline-block" : "none";
}
/**
* @param {String} text
* @param {Number} delay
*/
async setStatusText(text, delay) {
if (delay !== undefined && delay > 0) {
await Utils.sleep(delay);
}
document.getElementById("favorites-load-status-label").textContent = text;
}
resetMatchCount() {
this.updateMatchCount(0);
}
/**
* @param {Number} value
*/
updateMatchCount(value) {
if (!this.matchCountLabelExists) {
return;
}
this.searchResultCount = value === undefined ? this.getSearchResults(this.allFavorites).length : value;
const suffix = this.searchResultCount === 1 ? "Match" : "Matches";
this.matchCountLabel.textContent = `${this.searchResultCount} ${suffix}`;
}
/**
* @param {Number} value
*/
incrementMatchCount(value) {
if (!this.matchCountLabelExists) {
return;
}
this.searchResultCount += value === undefined ? 1 : value;
this.matchCountLabel.textContent = `${this.searchResultCount} Matches`;
}
/**
* @param {Post[]} newPosts
*/
async insertNewFavorites(newPosts) {
const searchCommand = new SearchCommand(this.finalSearchQuery);
const insertedPosts = [];
const metadataPopulateWaitTime = 1000;
newPosts.reverse();
if (this.allowedRatings !== 7) {
await Utils.sleep(metadataPopulateWaitTime);
}
for (const post of newPosts) {
if (this.matchesSearchAndRating(searchCommand, post)) {
this.paginator.insertNewFavorite(post);
insertedPosts.push(post);
}
}
this.paginator.createPaginationMenu(this.paginator.currentPageNumber, this.getFavoritesMatchedByLastSearch);
setTimeout(() => {
dispatchEvent(new CustomEvent("newFavoritesFetchedOnReload", {
detail: {
empty: false,
thumbs: insertedPosts.map(post => post.root)
}
}));
}, 250);
dispatchEvent(new CustomEvent("newSearchResults", {
detail: this.latestSearchResults
}));
}
/**
* @param {Post[]} favorites
*/
addFetchedFavoritesToContent(favorites) {
this.paginator.paginateWhileFetching(favorites);
}
/**
* @param {Post[]} searchResults
*/
paginateSearchResults(searchResults) {
if (!this.searchFlags.aNewSearchCouldProduceDifferentResults) {
return;
}
searchResults = this.sortPosts(searchResults);
searchResults = this.getResultsWithAllowedRatings(searchResults);
this.latestSearchResults = searchResults;
this.updateMatchCount(searchResults.length);
this.paginator.paginate(searchResults);
this.searchFlags.resetFlagsImplyingDifferentSearchResults();
dispatchEvent(new CustomEvent("newSearchResults", {
detail: searchResults
}));
}
/**
* @param {Boolean} value
*/
toggleTagBlacklistExclusion(value) {
FavoritesLoader.tagNegation.useTagBlacklist = value;
this.searchFlags.excludeBlacklistWasClicked = true;
}
/**
* @param {Number} value
*/
updateResultsPerPage(value) {
this.paginator.maxFavoritesPerPage = value;
this.searchFlags.recentlyChangedResultsPerPage = true;
this.searchFavorites();
}
/**
* @param {Post[]} posts
* @returns {Post[]}
*/
sortPosts(posts) {
if (this.searchFlags.searchResultsAreShuffled) {
return posts;
}
const sortedPosts = posts.slice();
const sortingMethod = Utils.getSortingMethod();
if (sortingMethod !== "default") {
sortedPosts.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()) {
sortedPosts.reverse();
}
return sortedPosts;
}
/**
* @returns {Boolean}
*/
sortAscending() {
const sortFavoritesAscending = document.getElementById("sort-ascending");
return sortFavoritesAscending === null ? false : sortFavoritesAscending.checked;
}
onSortingParametersChanged() {
this.searchFlags.sortingParametersWereChanged = true;
const matchedPosts = this.getFavoritesMatchedByLastSearch;
this.paginateSearchResults(matchedPosts);
dispatchEvent(new Event("sortingParametersChanged"));
}
/**
* @param {Number} allowedRatings
*/
onAllowedRatingsChanged(allowedRatings) {
this.allowedRatings = allowedRatings;
this.searchFlags.allowedRatingsWereChanged = true;
const matchedPosts = this.getFavoritesMatchedByLastSearch;
this.paginateSearchResults(matchedPosts);
}
/**
* @returns {Boolean}
*/
allRatingsAreAllowed() {
return this.allowedRatings === 7;
}
/**
* @param {Post} post
* @returns {Boolean}
*/
ratingIsAllowed(post) {
if (this.allRatingsAreAllowed()) {
return true;
}
// eslint-disable-next-line no-bitwise
return (post.metadata.rating & this.allowedRatings) > 0;
}
/**
* @param {Post[]} searchResults
* @returns {Post[]}
*/
getResultsWithAllowedRatings(searchResults) {
if (this.allRatingsAreAllowed()) {
return searchResults;
}
return searchResults.filter(post => this.ratingIsAllowed(post));
}
/**
* @param {SearchCommand} searchCommand
* @param {Post} post
* @returns {Boolean}
*/
matchesSearchAndRating(searchCommand, post) {
return this.ratingIsAllowed(post) && searchCommand.matches(post);
}
/**
* @param {String} id
*/
findFavorite(id) {
this.paginator.findFavorite(id);
}
}
class FavoritesMenu {
static uiHTML = `
<div id="favorites-search-gallery-menu" class="light-green-gradient not-highlightable">
<style>
#favorites-search-gallery-menu {
position: sticky;
top: 0;
padding: 10px;
z-index: 30;
margin-bottom: 10px;
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
appearance: none;
margin: 0;
}
}
#favorites-search-gallery-menu-panels {
>div {
flex: 1;
}
}
#left-favorites-panel {
flex: 10 !important;
>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 {
flex: 9 !important;
margin-left: 30px;
display: none;
}
textarea {
max-width: 100%;
height: 50px;
width: 99%;
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);
}
}
.add-or-remove-button {
position: absolute;
left: 0;
top: 0;
width: 40%;
font-weight: bold;
background: none;
border: none;
z-index: 2;
filter: grayscale(70%);
&:active,
&:hover {
filter: none !important;
}
}
.remove-favorite-button {
color: red;
}
.add-favorite-button {
>svg {
fill: hotpink;
}
}
.statistic-hint {
position: absolute;
z-index: 3;
text-align: center;
right: 0;
top: 0;
background: white;
color: #0075FF;
font-weight: bold;
/* font-size: 18px; */
pointer-events: none;
font-size: calc(8px + (20 - 8) * ((100vw - 300px) / (3840 - 300)));
width: 55%;
padding: 2px 0px;
border-bottom-left-radius: 4px;
}
img {
-webkit-user-drag: none;
-khtml-user-drag: none;
-moz-user-drag: none;
-o-user-drag: none;
}
.favorite {
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 {
display: block;
overflow: hidden;
position: relative;
cursor: default;
>img:first-child {
width: 100%;
z-index: 1;
}
/* &:has(.add-or-remove-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);
}
}
#column-resize-container {
>div {
align-content: center;
}
}
#find-favorite {
margin-top: 7px;
>input {
width: 75px;
/* border-radius: 6px;
height: 35px;
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;
}
}
}
#favorites-search-gallery-content {
padding: 0px 20px 30px 20px;
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;
right: 0;
transform: translateX(25%);
font-style: normal;
font-weight: normal;
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;
}
h5,
h6 {
color: rgb(255, 0, 255);
}
}
.hotkey {
font-weight: bolder;
color: orange;
}
#left-favorites-panel-bottom-row {
display: flex;
margin-top: 10px;
flex-wrap: nowrap;
>div {
flex: 1;
}
.number {
font-size: 16px;
>input {
width: 5ch;
}
}
}
#additional-favorite-options {
>div:not(:last-child) {
padding-bottom: 10px;
}
select {
cursor: pointer;
min-height: 25px;
}
}
.number-label-container {
display: inline-block;
min-width: 130px;
}
#performance-profile {
width: 150px;
}
#show-ui-div {
&.ui-hidden {
max-width: 100vw;
text-align: center;
align-content: center;
}
}
#rating-container {
white-space: nowrap;
}
#allowed-ratings {
margin-top: 5px;
font-size: 12px;
>label {
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;
}
}
}
.add-or-remove-button {
visibility: hidden;
cursor: pointer;
}
#favorites-load-status {
>label {
display: inline-block;
width: 140px;
}
}
#favorites-load-status-label {
/* color: #3498db; */
padding-left: 20px;
}
#main-favorite-options-container {
display: flex;
flex-wrap: wrap;
flex-direction: row;
>div {
flex-basis: 45%;
}
}
#sort-ascending {
position: absolute;
top: -2px;
left: 150px;
}
#find-favorite-input {
border: none !important;
}
div#header {
margin-bottom: 0 !important;
}
body {
&:fullscreen,
&::backdrop {
background-color: var(--c-bg);
}
}
</style>
<div id="favorites-search-gallery-menu-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-load-status-label"></label>
</span>
<div id="left-favorites-panel-top-row">
<button title="Search favorites
ctrl+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>
<button title="Delete cached favorites and reset 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.17.2:</h4>
<h5>Features:</h5>
<ul>
<li>Redirect to original image</li>
<ul>
<li>Ctrl+Click on thumbnail: redirect to original image, but stay on current tab</li>
<li>Ctrl+Shift+Click on thumbnail: redirect to original image</li>
<li>Works on both favorites and search pages</li>
<li>Works both in and out of gallery</li>
</ul>
</ul>
<h4>1.17:</h4>
<h5>Features:</h5>
<ul>
<li>Added autoplay</li>
<li>Added new hotkeys and hints for them</li>
<li>Gallery now auto changes to next/previous page rather tha looping to start of same page</li>
<ul>
<li sty>Basically, you can view every single favorite without ever exiting the gallery</li>
</ul>
<li>Middle click on tag in "details" to quickly search for it</li>
<li>Changed UI</li>
</ul>
<h5> Notes/Fixes:</h5>
<ul>
<li>
<strong>
A large site update is ongoing, creating new bugs
</strong>
</li>
<li>I'm fixing anything I find, but please report any issues you find also</li>
</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="bottom-panel-1">
<label class="checkbox" title="Show more options">
<input type="checkbox" id="options-checkbox"> More Options
<span class="option-hint"> (O)</span>
</label>
<div class="options-container">
<div id="main-favorite-options-container">
<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
<span class="option-hint"> (R)</span>
</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
<span class="option-hint"> (R)</span>
</label>
</div>
<div>
<label class="checkbox" title="Exclude favorites with 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 style="display: none;">
<label class="checkbox" title="Enable fancy image hovering (experimental)">
<input type="checkbox" id="statistic-hint-checkbox"> Statistics
<span class="option-hint"> (S)</span>
</label>
</div>
<div id="show-hints-container">
<label class="checkbox" title="Show hotkeys and shortcuts">
<input type="checkbox" id="show-hints-checkbox"> Hotkey Hints
<span class="option-hint"> (H)</span>
</label>
</div>
</div>
<div id="dynamic-favorite-options">
</div>
</div>
</div>
</div>
<div id="bottom-panel-2">
<div id="additional-favorite-options-container" class="options-container">
<div id="additional-favorite-options">
<div id="sort-container" title="Change sorting order of search results">
<label style="margin-right: 22px;" for="sorting-method">Sort By</label>
<label style="margin-left: 22px;" for="sort-ascending">Ascending</label>
<div style="position: relative;">
<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>
</select>
<input type="checkbox" id="sort-ascending">
</div>
</div>
<div>
<div id="results-per-page-container" style="display: inline-block;"
title="Set the maximum number of search results to display on each page
Lower numbers improve responsiveness">
<span class="number-label-container">
<label id="results-per-page-label" for="results-per-page-input">Results per Page</label>
</span>
<br>
<span class="number">
<hold-button class="number-arrow-down" pollingtime="50">
<span><</span>
</hold-button>
<input type="number" id="results-per-page-input" min="100" max="10000" step="50">
<hold-button class="number-arrow-up" pollingtime="50">
<span>></span>
</hold-button>
</span>
</div>
<div id="column-resize-container" title="Set the number of favorites per row"
style="display: inline-block;">
<div>
<span class="number-label-container">
<label>Columns</label>
</span>
<br>
<span class="number">
<hold-button class="number-arrow-down" pollingtime="50">
<span><</span>
</hold-button>
<input type="number" id="column-resize-input" min="2" max="20">
<hold-button class="number-arrow-up" pollingtime="50">
<span>></span>
</hold-button>
</span>
</div>
</div>
</div>
<div id="rating-container" title="Filter search results by rating">
<label>Rating</label>
<br>
<div id="allowed-ratings" class="not-highlightable">
<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>
</div>
</div>
<div id="bottom-panel-3">
<div id="show-ui-div">
<label class="checkbox" title="Toggle UI">
<input type="checkbox" id="show-ui">UI
<span class="option-hint"> (U)</span>
</label>
</div>
<div class="options-container">
<span id="find-favorite" style="display: none;">
<button title="Find favorite favorite using its ID" id="find-favorite-button"
style="white-space: nowrap;">Find</button>
<input type="number" id="find-favorite-input" placeholder="ID">
</span>
</div>
</div>
<div id="bottom-panel-4">
</div>
</div>
</div>
<div id="right-favorites-panel"></div>
</div>
<div class="loading-wheel" id="loading-wheel" style="display: none;"></div>
</div>
`;
static get disabled() {
return !Utils.onFavoritesPage();
}
static {
Utils.addStaticInitializer(() => {
if (Utils.onFavoritesPage()) {
Utils.insertFavoritesSearchGalleryHTML("afterbegin", FavoritesMenu.uiHTML);
}
});
}
/**
* @type {Number}
*/
maxSearchHistoryLength;
/**
* @type {Object.<PropertyKey, String>}
*/
preferences;
/**
* @type {Object.<PropertyKey, String>}
*/
localStorageKeys;
/**
* @type {Object.<PropertyKey, HTMLButtonElement>}
*/
buttons;
/**
* @type {Object.<PropertyKey, HTMLInputElement}
*/
checkboxes;
/**
* @type {Object.<PropertyKey, HTMLInputElement}
*/
inputs;
/**
* @type {Cooldown}
*/
columnWheelResizeCaptionCooldown;
/**
* @type {String[]}
*/
searchHistory;
/**
* @type {Number}
*/
searchHistoryIndex;
/**
* @type {String}
*/
lastSearchQuery;
constructor() {
if (FavoritesMenu.disabled) {
return;
}
this.configureMobileUI();
this.initializeFields();
this.extractUIElements();
this.setMainButtonInteractability(false);
this.addEventListenersToFavoritesPage();
this.loadFavoritesPagePreferences();
this.removePaginatorFromFavoritesPage();
this.configureAddOrRemoveButtonOptionVisibility();
this.configureDesktopUI();
this.addEventListenersToWhatsNewMenu();
this.addHintsOption();
}
initializeFields() {
this.maxSearchHistoryLength = 100;
this.searchHistory = [];
this.searchHistoryIndex = 0;
this.lastSearchQuery = "";
this.preferences = {
showAddOrRemoveButtons: Utils.userIsOnTheirOwnFavoritesPage() ? "showRemoveButtons" : "showAddFavoriteButtons",
showOptions: "showOptions",
excludeBlacklist: "excludeBlacklist",
searchHistory: "favoritesSearchHistory",
findFavorite: "findFavorite",
thumbSize: "thumbSize",
columnCount: "columnCount",
showUI: "showUI",
performanceProfile: "performanceProfile",
resultsPerPage: "resultsPerPage",
fancyImageHovering: "fancyImageHovering",
enableOnSearchPages: "enableOnSearchPages",
sortAscending: "sortAscending",
sortingMethod: "sortingMethod",
allowedRatings: "allowedRatings",
showHotkeyHints: "showHotkeyHints",
showStatisticHints: "showStatisticHints"
};
this.localStorageKeys = {
searchHistory: "favoritesSearchHistory"
};
this.columnWheelResizeCaptionCooldown = new Cooldown(500, true);
}
extractUIElements() {
this.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")
};
this.checkboxes = {
showOptions: document.getElementById("options-checkbox"),
showAddOrRemoveButtons: Utils.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"),
showHotkeyHints: document.getElementById("show-hints-checkbox"),
showStatisticHints: document.getElementById("statistic-hint-checkbox")
};
this.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")
};
}
loadFavoritesPagePreferences() {
const userIsLoggedIn = Utils.getUserId() !== null;
const showAddOrRemoveButtonsDefault = !Utils.userIsOnTheirOwnFavoritesPage() && userIsLoggedIn;
const addOrRemoveFavoriteButtonsAreVisible = Utils.getPreference(this.preferences.showAddOrRemoveButtons, showAddOrRemoveButtonsDefault);
this.checkboxes.showAddOrRemoveButtons.checked = addOrRemoveFavoriteButtonsAreVisible;
setTimeout(() => {
this.toggleAddOrRemoveButtons();
}, 100);
const showOptions = Utils.getPreference(this.preferences.showOptions, false);
this.checkboxes.showOptions.checked = showOptions;
this.toggleFavoritesOptions(showOptions);
if (Utils.userIsOnTheirOwnFavoritesPage()) {
this.checkboxes.filterBlacklist.checked = Utils.getPreference(this.preferences.excludeBlacklist, false);
favoritesLoader.toggleTagBlacklistExclusion(this.checkboxes.filterBlacklist.checked);
} else {
this.checkboxes.filterBlacklist.checked = true;
this.checkboxes.filterBlacklist.parentElement.style.display = "none";
}
this.searchHistory = JSON.parse(localStorage.getItem(this.localStorageKeys.searchHistory)) || [];
if (this.searchHistory.length > 0) {
this.inputs.searchBox.value = this.searchHistory[0];
}
this.inputs.findFavorite.value = Utils.getPreference(this.preferences.findFavorite, "");
this.inputs.columnCount.value = Utils.getPreference(this.preferences.columnCount, Utils.defaults.columnCount);
this.changeColumnCount(this.inputs.columnCount.value);
const showUI = Utils.getPreference(this.preferences.showUI, true);
this.checkboxes.showUI.checked = showUI;
this.toggleUI(showUI);
const performanceProfile = Utils.getPerformanceProfile();
for (const option of this.inputs.performanceProfile.children) {
if (parseInt(option.value) === performanceProfile) {
option.selected = "selected";
}
}
const resultsPerPage = parseInt(Utils.getPreference(this.preferences.resultsPerPage, Utils.defaults.resultsPerPage));
this.changeResultsPerPage(resultsPerPage);
if (Utils.onMobileDevice()) {
Utils.toggleFancyImageHovering(false);
this.checkboxes.fancyImageHovering.parentElement.style.display = "none";
this.checkboxes.enableOnSearchPages.parentElement.style.display = "none";
} else {
const fancyImageHovering = Utils.getPreference(this.preferences.fancyImageHovering, false);
this.checkboxes.fancyImageHovering.checked = fancyImageHovering;
Utils.toggleFancyImageHovering(fancyImageHovering);
}
this.checkboxes.enableOnSearchPages.checked = Utils.getPreference(this.preferences.enableOnSearchPages, false);
this.checkboxes.sortAscending.checked = Utils.getPreference(this.preferences.sortAscending, false);
const sortingMethod = Utils.getPreference(this.preferences.sortingMethod, "default");
for (const option of this.inputs.sortingMethod) {
if (option.value === sortingMethod) {
option.selected = "selected";
}
}
const allowedRatings = Utils.loadAllowedRatings();
// eslint-disable-next-line no-bitwise
this.checkboxes.explicitRating.checked = (allowedRatings & 4) === 4;
// eslint-disable-next-line no-bitwise
this.checkboxes.questionableRating.checked = (allowedRatings & 2) === 2;
// eslint-disable-next-line no-bitwise
this.checkboxes.safeRating.checked = (allowedRatings & 1) === 1;
this.preventUserFromUncheckingAllRatings(allowedRatings);
const showStatisticHints = Utils.getPreference(this.preferences.showStatisticHints, false);
this.checkboxes.showStatisticHints.checked = showStatisticHints;
this.toggleStatisticHints(showStatisticHints);
}
removePaginatorFromFavoritesPage() {
if (!Utils.onFavoritesPage()) {
return;
}
const paginator = document.getElementById("paginator");
const pi = document.getElementById("pi");
if (paginator !== null) {
paginator.remove();
}
if (pi !== null) {
pi.remove();
}
}
addEventListenersToFavoritesPage() {
this.buttons.search.onclick = (event) => {
const query = this.inputs.searchBox.value;
if (event.ctrlKey) {
const queryWithFormattedIds = query.replace(/(?:^|\s)(\d+)(?:$|\s)/g, " id:$1 ");
Utils.openSearchPage(queryWithFormattedIds);
} else {
Utils.hideAwesomplete(this.inputs.searchBox);
favoritesLoader.searchFavorites(query);
this.addToFavoritesSearchHistory(query);
}
};
this.buttons.search.addEventListener("contextmenu", (event) => {
const queryWithFormattedIds = this.inputs.searchBox.value.replace(/(?:^|\s)(\d+)(?:$|\s)/g, " id:$1 ");
Utils.openSearchPage(queryWithFormattedIds);
event.preventDefault();
});
this.inputs.searchBox.addEventListener("keydown", (event) => {
switch (event.key) {
case "Enter":
if (Utils.awesompleteIsUnselected(this.inputs.searchBox)) {
event.preventDefault();
this.buttons.search.click();
} else {
Utils.clearAwesompleteSelection(this.inputs.searchBox);
}
break;
case "ArrowUp":
case "ArrowDown":
if (Utils.awesompleteIsVisible(this.inputs.searchBox)) {
this.updateLastSearchQuery();
} else {
event.preventDefault();
this.traverseFavoritesSearchHistory(event.key);
}
break;
default:
this.updateLastSearchQuery();
break;
}
});
this.inputs.searchBox.addEventListener("wheel", (event) => {
if (event.shiftKey || event.ctrlKey) {
return;
}
const direction = event.deltaY > 0 ? "ArrowDown" : "ArrowUp";
this.traverseFavoritesSearchHistory(direction);
event.preventDefault();
});
this.checkboxes.showOptions.onchange = () => {
this.toggleFavoritesOptions(this.checkboxes.showOptions.checked);
Utils.setPreference(this.preferences.showOptions, this.checkboxes.showOptions.checked);
};
this.checkboxes.showAddOrRemoveButtons.onchange = () => {
this.toggleAddOrRemoveButtons();
Utils.setPreference(this.preferences.showAddOrRemoveButtons, this.checkboxes.showAddOrRemoveButtons.checked);
};
this.buttons.shuffle.onclick = () => {
favoritesLoader.shuffleSearchResults();
};
this.buttons.clear.onclick = () => {
this.inputs.searchBox.value = "";
};
this.checkboxes.filterBlacklist.onchange = () => {
Utils.setPreference(this.preferences.excludeBlacklist, this.checkboxes.filterBlacklist.checked);
favoritesLoader.toggleTagBlacklistExclusion(this.checkboxes.filterBlacklist.checked);
favoritesLoader.searchFavorites();
};
this.buttons.invert.onclick = () => {
favoritesLoader.invertSearchResults();
};
this.buttons.reset.onclick = () => {
Utils.deletePersistentData();
};
this.inputs.findFavorite.addEventListener("keydown", (event) => {
if (event.key === "Enter") {
this.buttons.findFavorite.click();
}
});
this.buttons.findFavorite.onclick = () => {
favoritesLoader.findFavorite(this.inputs.findFavorite.value);
Utils.setPreference(this.preferences.findFavorite, this.inputs.findFavorite.value);
};
this.inputs.columnCount.onchange = () => {
this.changeColumnCount(parseInt(this.inputs.columnCount.value));
};
this.checkboxes.showUI.onchange = () => {
this.toggleUI(this.checkboxes.showUI.checked);
};
this.inputs.performanceProfile.onchange = () => {
Utils.setPreference(this.preferences.performanceProfile, parseInt(this.inputs.performanceProfile.value));
window.location.reload();
};
this.inputs.resultsPerPage.onchange = () => {
this.changeResultsPerPage(parseInt(this.inputs.resultsPerPage.value), false);
};
if (!Utils.onMobileDevice()) {
this.checkboxes.fancyImageHovering.onchange = () => {
Utils.toggleFancyImageHovering(this.checkboxes.fancyImageHovering.checked);
Utils.setPreference(this.preferences.fancyImageHovering, this.checkboxes.fancyImageHovering.checked);
};
}
this.checkboxes.enableOnSearchPages.onchange = () => {
Utils.setPreference(this.preferences.enableOnSearchPages, this.checkboxes.enableOnSearchPages.checked);
};
this.checkboxes.sortAscending.onchange = () => {
Utils.setPreference(this.preferences.sortAscending, this.checkboxes.sortAscending.checked);
favoritesLoader.onSortingParametersChanged();
};
this.inputs.sortingMethod.onchange = () => {
Utils.setPreference(this.preferences.sortingMethod, this.inputs.sortingMethod.value);
favoritesLoader.onSortingParametersChanged();
};
this.inputs.allowedRatings.onchange = () => {
this.changeAllowedRatings();
};
window.addEventListener("wheel", (event) => {
if (!event.shiftKey) {
return;
}
const delta = (event.wheelDelta ? event.wheelDelta : -event.deltaY);
const columnAddend = delta > 0 ? -1 : 1;
if (this.columnWheelResizeCaptionCooldown.ready) {
Utils.forceHideCaptions(true);
}
this.changeColumnCount(parseInt(this.inputs.columnCount.value) + columnAddend);
}, {
passive: true
});
this.columnWheelResizeCaptionCooldown.onDebounceEnd = () => {
Utils.forceHideCaptions(false);
};
this.columnWheelResizeCaptionCooldown.onCooldownEnd = () => {
if (!this.columnWheelResizeCaptionCooldown.debouncing) {
Utils.forceHideCaptions(false);
}
};
window.addEventListener("readyToSearch", () => {
this.setMainButtonInteractability(true);
}, {
once: true
});
document.addEventListener("keydown", (event) => {
if (!Utils.isHotkeyEvent(event)) {
return;
}
switch (event.key.toLowerCase()) {
case "r":
this.checkboxes.showAddOrRemoveButtons.click();
break;
case "u":
this.checkboxes.showUI.click();
break;
case "o":
this.checkboxes.showOptions.click();
break;
case "h":
this.checkboxes.showHotkeyHints.click();
break;
case "s":
// this.FAVORITE_CHECKBOXES.showStatisticHints.click();
break;
default:
break;
}
}, {
passive: true
});
window.addEventListener("load", () => {
this.inputs.searchBox.focus();
}, {
once: true
});
this.checkboxes.showStatisticHints.onchange = () => {
this.toggleStatisticHints(this.checkboxes.showStatisticHints.checked);
Utils.setPreference(this.preferences.showStatisticHints, this.checkboxes.showStatisticHints.checked);
};
window.addEventListener("searchForTag", (event) => {
this.inputs.searchBox.value = event.detail;
this.buttons.search.click();
});
}
configureAddOrRemoveButtonOptionVisibility() {
this.checkboxes.showAddOrRemoveButtons.parentElement.parentElement.style.display = "block";
}
updateLastSearchQuery() {
if (this.inputs.searchBox.value !== this.lastSearchQuery) {
this.lastSearchQuery = this.inputs.searchBox.value;
}
this.searchHistoryIndex = -1;
}
/**
* @param {String} newSearch
*/
addToFavoritesSearchHistory(newSearch) {
newSearch = newSearch.trim();
this.searchHistory = this.searchHistory.filter(search => search !== newSearch);
this.searchHistory.unshift(newSearch);
this.searchHistory.length = Math.min(this.searchHistory.length, this.maxSearchHistoryLength);
localStorage.setItem(this.localStorageKeys.searchHistory, JSON.stringify(this.searchHistory));
}
/**
* @param {String} direction
*/
traverseFavoritesSearchHistory(direction) {
if (this.searchHistory.length > 0) {
if (direction === "ArrowUp") {
this.searchHistoryIndex = Math.min(this.searchHistoryIndex + 1, this.searchHistory.length - 1);
} else {
this.searchHistoryIndex = Math.max(this.searchHistoryIndex - 1, -1);
}
if (this.searchHistoryIndex === -1) {
this.inputs.searchBox.value = this.lastSearchQuery;
} else {
this.inputs.searchBox.value = this.searchHistory[this.searchHistoryIndex];
}
}
}
/**
* @param {Boolean} value
*/
toggleFavoritesOptions(value) {
document.querySelectorAll(".options-container").forEach((option) => {
option.style.display = value ? "block" : "none";
});
}
toggleAddOrRemoveButtons() {
const value = this.checkboxes.showAddOrRemoveButtons.checked;
this.toggleAddOrRemoveButtonVisibility(value);
Utils.toggleThumbHoverOutlines(value);
Utils.forceHideCaptions(value);
if (!value) {
dispatchEvent(new Event("captionOverrideEnd"));
}
}
/**
* @param {Boolean} value
*/
toggleAddOrRemoveButtonVisibility(value) {
const visibility = value ? "visible" : "hidden";
Utils.insertStyleHTML(`
.add-or-remove-button {
visibility: ${visibility} !important;
}
`, "add-or-remove-button-visibility");
}
/**
* @param {Number} count
*/
changeColumnCount(count) {
count = parseInt(count);
if (isNaN(count)) {
this.inputs.columnCount.value = Utils.getPreference(this.preferences.columnCount, Utils.defaults.columnCount);
return;
}
count = Utils.clamp(parseInt(count), 4, 20);
Utils.insertStyleHTML(`
#favorites-search-gallery-content {
grid-template-columns: repeat(${count}, 1fr) !important;
}
`, "column-count");
this.inputs.columnCount.value = count;
Utils.setPreference(this.preferences.columnCount, count);
}
/**
* @param {Number} resultsPerPage
*/
changeResultsPerPage(resultsPerPage) {
resultsPerPage = parseInt(resultsPerPage);
if (isNaN(resultsPerPage)) {
this.inputs.resultsPerPage.value = Utils.getPreference(this.preferences.resultsPerPage, Utils.defaults.resultsPerPage);
return;
}
resultsPerPage = Utils.clamp(resultsPerPage, 50, 5000);
this.inputs.resultsPerPage.value = resultsPerPage;
Utils.setPreference(this.preferences.resultsPerPage, resultsPerPage);
favoritesLoader.updateResultsPerPage(resultsPerPage);
}
/**
* @param {Boolean} value
*/
toggleUI(value) {
const menu = document.getElementById("favorites-search-gallery-menu");
const menuPanels = document.getElementById("favorites-search-gallery-menu-panels");
const header = document.getElementById("header");
const showUIDiv = document.getElementById("show-ui-div");
const showUIContainer = document.getElementById("bottom-panel-3");
if (value) {
header.style.display = "";
showUIContainer.insertAdjacentElement("afterbegin", showUIDiv);
menuPanels.style.display = "flex";
menu.removeAttribute("style");
} else {
menu.appendChild(showUIDiv);
header.style.display = "none";
menuPanels.style.display = "none";
menu.style.background = getComputedStyle(document.body).background;
}
showUIDiv.classList.toggle("ui-hidden", !value);
Utils.setPreference(this.preferences.showUI, value);
}
configureMobileUI() {
if (!Utils.onMobileDevice()) {
return;
}
Utils.insertStyleHTML(`
#performance-profile-container, #show-hints-container {
display: none !important;
}
.thumb, .favorite {
> div > canvas {
display: none;
}
}
.checkbox {
input[type="checkbox"] {
margin-right: 10px;
}
}
#favorites-search-gallery-menu-panels {
>div {
textarea {
width: 95% !important;
}
}
}
#mobile-container {
position: fixed !important;
z-index: 30;
width: 100vw;
}
#show-ui-div {
display: none;
}
#favorites-search-gallery-menu-panels {
display: block !important;
}
#right-favorites-panel {
margin-left: 0px !important;
}
#left-favorites-panel-bottom-row {
display: block !important;
margin-left: 10px !important;
}
input[type="checkbox"] {
width: 25px;
height: 25px;
}
}
#sort-ascending {
width: 25px;
height: 25px;
}
`, "mobile");
const mobileUIContainer = document.createElement("div");
mobileUIContainer.id = "mobile-container";
mobileUIContainer.appendChild(document.getElementById("header"));
mobileUIContainer.appendChild(document.getElementById("favorites-search-gallery-menu"));
Utils.insertFavoritesSearchGalleryHTML("afterbegin", mobileUIContainer.innerHTML);
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>";
}
}
configureDesktopUI() {
if (Utils.onMobileDevice()) {
return;
}
Utils.insertStyleHTML(`
.checkbox {
&:hover {
color: #000;
background: #93b393;
text-shadow: none;
cursor: pointer;
}
input[type="checkbox"] {
width: 20px;
height: 20px;
}
}
#sort-ascending {
width: 20px;
height: 20px;
}
`, "desktop");
}
addEventListenersToWhatsNewMenu() {
if (Utils.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");
};
}
changeAllowedRatings() {
let allowedRatings = 0;
if (this.checkboxes.explicitRating.checked) {
allowedRatings += 4;
}
if (this.checkboxes.questionableRating.checked) {
allowedRatings += 2;
}
if (this.checkboxes.safeRating.checked) {
allowedRatings += 1;
}
Utils.setPreference(this.preferences.allowedRatings, allowedRatings);
favoritesLoader.onAllowedRatingsChanged(allowedRatings);
this.preventUserFromUncheckingAllRatings(allowedRatings);
}
/**
* @param {Number} allowedRatings
*/
preventUserFromUncheckingAllRatings(allowedRatings) {
if (allowedRatings === 4) {
this.checkboxes.explicitRating.nextElementSibling.style.pointerEvents = "none";
} else if (allowedRatings === 2) {
this.checkboxes.questionableRating.nextElementSibling.style.pointerEvents = "none";
} else if (allowedRatings === 1) {
this.checkboxes.safeRating.nextElementSibling.style.pointerEvents = "none";
} else {
this.checkboxes.explicitRating.nextElementSibling.removeAttribute("style");
this.checkboxes.questionableRating.nextElementSibling.removeAttribute("style");
this.checkboxes.safeRating.nextElementSibling.removeAttribute("style");
}
}
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;
}
}
/**
* @param {Boolean} value
*/
toggleOptionHints(value) {
const html = value ? "" : ".option-hint {display:none;}";
Utils.insertStyleHTML(html, "option-hint-visibility");
}
async addHintsOption() {
this.toggleOptionHints(false);
await Utils.sleep(50);
if (Utils.onMobileDevice()) {
return;
}
const optionHintsEnabled = Utils.getPreference(this.preferences.showHotkeyHints, false);
this.checkboxes.showHotkeyHints.checked = optionHintsEnabled;
this.checkboxes.showHotkeyHints.onchange = () => {
this.toggleOptionHints(this.checkboxes.showHotkeyHints.checked);
Utils.setPreference(this.preferences.showHotkeyHints, this.checkboxes.showHotkeyHints.checked);
};
this.toggleOptionHints(optionHintsEnabled);
}
/**
* @param {Boolean} value
*/
toggleStatisticHints(value) {
const html = value ? "" : ".statistic-hint {display:none;}";
Utils.insertStyleHTML(html, "statistic-hint-visibility");
}
}
class AutoplayListenerList {
/**
* @type {Function}
*/
onEnable;
/**
* @type {Function}
*/
onDisable;
/**
* @type {Function}
*/
onPause;
/**
* @type {Function}
*/
onResume;
/**
* @type {Function}
*/
onComplete;
/**
* @type {Function}
*/
onVideoEndedBeforeMinimumViewTime;
/**
* @param {Function} onEnable
* @param {Function} onDisable
* @param {Function} onPause
* @param {Function} onResume
* @param {Function} onComplete
* @param {Function} onVideoEndedEarly
*/
constructor(onEnable, onDisable, onPause, onResume, onComplete, onVideoEndedEarly) {
this.onEnable = onEnable;
this.onDisable = onDisable;
this.onPause = onPause;
this.onResume = onResume;
this.onComplete = onComplete;
this.onVideoEndedBeforeMinimumViewTime = onVideoEndedEarly;
}
}
class Autoplay {
static autoplayHTML = `
<div id="autoplay-container">
<style>
#autoplay-container {
visibility: hidden;
}
#autoplay-menu {
position: fixed;
left: 50%;
transform: translate(-50%);
bottom: 5%;
padding: 0;
margin: 0;
background: rgba(40, 40, 40, 1);
border-radius: 4px;
white-space: nowrap;
z-index: 10000;
opacity: 0;
transition: opacity .25s ease-in-out;
&.visible {
opacity: 1;
}
&.persistent {
opacity: 1 !important;
visibility: visible !important;
}
>div>img {
color: red;
position: relative;
height: 75px;
cursor: pointer;
background-color: rgba(128, 128, 128, 0);
margin: 5px;
background-size: 10%;
z-index: 3;
border-radius: 4px;
&:hover {
background-color: rgba(200, 200, 200, .5);
}
}
}
.autoplay-progress-bar {
position: absolute;
top: 0;
left: 0;
width: 0%;
height: 100%;
background-color: steelblue;
z-index: 1;
}
#autoplay-video-progress-bar {
background-color: royalblue;
}
#autoplay-settings-menu {
visibility: hidden;
position: absolute;
top: 0;
left: 50%;
transform: translate(-50%, -105%);
border-radius: 4px;
font-size: 10px !important;
background: rgba(40, 40, 40, 1);
&.visible {
visibility: visible;
}
>div {
font-size: 30px;
display: flex;
justify-content: space-between;
align-items: center;
padding: 5px 10px;
color: white;
>label {
padding-right: 20px;
}
>.number {
background: none;
outline: 2px solid white;
>hold-button,
>button {
&::after {
width: 200%;
height: 130%;
}
}
>input[type="number"] {
color: white;
width: 7ch;
}
}
}
}
#autoplay-settings-button.settings-menu-opened {
filter: drop-shadow(6px 6px 3px #0075FF);
}
#autoplay-change-direction-mask {
filter: drop-shadow(2px 2px 3px #0075FF);
}
#autoplay-play-button:active {
filter: drop-shadow(2px 2px 10px #0075FF);
}
#autoplay-change-direction-mask-container {
pointer-events: none;
opacity: 0.75;
height: 75px;
width: 75px;
margin: 5px;
border-radius: 4px;
right: 0;
bottom: 0;
z-index: 4;
position: absolute;
clip-path: polygon(0% 0%, 0% 100%, 100% 100%);
&.upper-right {
clip-path: polygon(0% 0%, 100% 0%, 100% 100%);
}
}
.autoplay-settings-menu-label {
pointer-events: none;
}
</style>
<div id="autoplay-menu" class="not-highlightable">
<div id="autoplay-buttons">
<img id="autoplay-settings-button" title="Autoplay settings">
<img id="autoplay-play-button" title="Pause autoplay">
<img id="autoplay-change-direction-button" title="Change autoplay direction">
<div id="autoplay-change-direction-mask-container">
<img id="autoplay-change-direction-mask" title="Change autoplay direction">
</div>
</div>
<div id="autoplay-image-progress-bar" class="autoplay-progress-bar"></div>
<div id="autoplay-video-progress-bar" class="autoplay-progress-bar"></div>
<div id="autoplay-settings-menu">
<div>
<label for="autoplay-image-duration-input">Image/GIF Duration</label>
<span class="number">
<hold-button class="number-arrow-down" pollingtime="100"><span><</span></hold-button>
<input type="number" id="autoplay-image-duration-input" min="1" max="60" step="1">
<hold-button class="number-arrow-up" pollingtime="100"><span>></span></hold-button>
</span>
</div>
<div>
<label for="autoplay-minimum-video-duration-input">Minimum Video Duration</label>
<span class="number">
<hold-button class="number-arrow-down" pollingtime="100"><span><</span></hold-button>
<input type="number" id="autoplay-minimum-animated-duration-input" min="1" max="60" step="1">
<hold-button class="number-arrow-up" pollingtime="100"><span>></span></hold-button>
</span>
</div>
</div>
</div>
</div>
`;
static preferences = {
active: "autoplayActive",
paused: "autoplayPaused",
imageDuration: "autoplayImageDuration",
minimumVideoDuration: "autoplayMinimumVideoDuration",
direction: "autoplayForward"
};
static menuIconImageURLs = {
play: Utils.createObjectURLFromSvg(Utils.icons.play),
pause: Utils.createObjectURLFromSvg(Utils.icons.pause),
changeDirection: Utils.createObjectURLFromSvg(Utils.icons.changeDirection),
changeDirectionAlt: Utils.createObjectURLFromSvg(Utils.icons.changeDirectionAlt),
tune: Utils.createObjectURLFromSvg(Utils.icons.tune)
};
static settings = {
imageViewDuration: Utils.getPreference(Autoplay.preferences.imageDuration, 3000),
minimumVideoDuration: Utils.getPreference(Autoplay.preferences.minimumVideoDuration, 5000),
menuVisibilityDuration: 500,
moveForward: Utils.getPreference(Autoplay.preferences.direction, true),
get imageViewDurationInSeconds() {
return Utils.millisecondsToSeconds(this.imageViewDuration);
},
get minimumVideoDurationInSeconds() {
return Utils.millisecondsToSeconds(this.minimumVideoDuration);
}
};
/**
* @type {Boolean}
*/
static get disabled() {
return Utils.onMobileDevice();
}
/**
* @type {{
* container: HTMLDivElement,
* menu: HTMLDivElement,
* settingsButton: HTMLImageElement,
* settingsMenu: {
* container: HTMLDivElement
* imageDurationInput: HTMLInputElement,
* minimumVideoDurationInput: HTMLInputElement,
* }
* playButton: HTMLImageElement,
* changeDirectionButton: HTMLImageElement,
* changeDirectionMask: {
* container: HTMLDivElement,
* image: HTMLImageElement
* },
* imageProgressBar: HTMLDivElement
* videoProgressBar: HTMLDivElement
* }}
*/
ui;
/**
* @type {AutoplayListenerList}
*/
events;
/**
* @type {AbortController}
*/
eventListenersAbortController;
/**
* @type {HTMLElement}
*/
currentThumb;
/**
* @type {Cooldown}
*/
imageViewTimer;
/**
* @type {Cooldown}
*/
menuVisibilityTimer;
/**
* @type {Cooldown}
*/
videoViewTimer;
/**
* @type {Boolean}
*/
active;
/**
* @type {Boolean}
*/
paused;
/**
* @type {Boolean}
*/
menuIsPersistent;
/**
* @type {Boolean}
*/
menuIsVisible;
/**
* @param {AutoplayListenerList} events
*/
constructor(events) {
if (Autoplay.disabled) {
return;
}
this.initializeEvents(events);
this.initializeFields();
this.initializeTimers();
this.insertHTML();
this.setMenuIconImageSources();
this.loadAutoplaySettingsIntoUI();
this.addEventListeners();
}
/**
* @param {AutoplayListenerList} events
*/
initializeEvents(events) {
this.events = events;
const onComplete = events.onComplete;
this.events.onComplete = () => {
if (this.active && !this.paused) {
onComplete();
}
};
}
initializeFields() {
this.ui = {
settingsMenu: {},
changeDirectionMask: {}
};
this.eventListenersAbortController = new AbortController();
this.currentThumb = null;
this.active = Utils.getPreference(Autoplay.preferences.active, false);
this.paused = Utils.getPreference(Autoplay.preferences.paused, false);
this.menuIsPersistent = false;
this.menuIsVisible = false;
}
initializeTimers() {
this.imageViewTimer = new Cooldown(Autoplay.settings.imageViewDuration);
this.menuVisibilityTimer = new Cooldown(Autoplay.settings.menuVisibilityDuration);
this.videoViewTimer = new Cooldown(Autoplay.settings.minimumVideoDuration);
this.imageViewTimer.onCooldownEnd = () => { };
this.menuVisibilityTimer.onCooldownEnd = () => {
this.hideMenu();
setTimeout(() => {
if (!this.menuIsPersistent && !this.menuIsVisible) {
this.toggleSettingMenu(false);
}
}, 100);
};
}
insertHTML() {
this.insertMenuHTML();
this.insertOptionHTML();
this.insertImageProgressHTML();
this.insertVideoProgressHTML();
}
insertMenuHTML() {
Utils.insertFavoritesSearchGalleryHTML("afterbegin", Autoplay.autoplayHTML);
this.ui.container = document.getElementById("autoplay-container");
this.ui.menu = document.getElementById("autoplay-menu");
this.ui.settingsButton = document.getElementById("autoplay-settings-button");
this.ui.settingsMenu.container = document.getElementById("autoplay-settings-menu");
this.ui.settingsMenu.imageDurationInput = document.getElementById("autoplay-image-duration-input");
this.ui.settingsMenu.minimumVideoDurationInput = document.getElementById("autoplay-minimum-animated-duration-input");
this.ui.playButton = document.getElementById("autoplay-play-button");
this.ui.changeDirectionButton = document.getElementById("autoplay-change-direction-button");
this.ui.changeDirectionMask.container = document.getElementById("autoplay-change-direction-mask-container");
this.ui.changeDirectionMask.image = document.getElementById("autoplay-change-direction-mask");
this.ui.imageProgressBar = document.getElementById("autoplay-image-progress-bar");
this.ui.videoProgressBar = document.getElementById("autoplay-video-progress-bar");
}
insertOptionHTML() {
Utils.createFavoritesOption(
"autoplay",
"Autoplay",
"Enable autoplay in gallery",
this.active,
(event) => {
this.toggle(event.target.checked);
},
true
);
}
insertImageProgressHTML() {
Utils.insertStyleHTML(`
#autoplay-image-progress-bar.animated {
transition: width ${Autoplay.settings.imageViewDurationInSeconds}s linear;
width: 100%;
}
`, "autoplay-image-progress-bar-animation");
}
insertVideoProgressHTML() {
Utils.insertStyleHTML(`
#autoplay-video-progress-bar.animated {
transition: width ${Autoplay.settings.minimumVideoDurationInSeconds}s linear;
width: 100%;
}
`, "autoplay-video-progress-bar-animation");
}
setMenuIconImageSources() {
this.ui.playButton.src = this.paused ? Autoplay.menuIconImageURLs.play : Autoplay.menuIconImageURLs.pause;
this.ui.settingsButton.src = Autoplay.menuIconImageURLs.tune;
this.ui.changeDirectionButton.src = Autoplay.menuIconImageURLs.changeDirection;
this.ui.changeDirectionMask.image.src = Autoplay.menuIconImageURLs.changeDirectionAlt;
this.ui.changeDirectionMask.container.classList.toggle("upper-right", Autoplay.settings.moveForward);
}
loadAutoplaySettingsIntoUI() {
this.ui.settingsMenu.imageDurationInput.value = Autoplay.settings.imageViewDurationInSeconds;
this.ui.settingsMenu.minimumVideoDurationInput.value = Autoplay.settings.minimumVideoDurationInSeconds;
}
addEventListeners() {
this.addMenuEventListeners();
this.addSettingsMenuEventListeners();
}
addMenuEventListeners() {
this.ui.settingsButton.onclick = () => {
this.toggleSettingMenu();
};
this.ui.playButton.onclick = () => {
this.pause();
};
this.ui.changeDirectionButton.onclick = () => {
Autoplay.settings.moveForward = !Autoplay.settings.moveForward;
this.ui.changeDirectionMask.container.classList.toggle("upper-right", Autoplay.settings.moveForward);
Utils.setPreference(Autoplay.preferences.direction, Autoplay.settings.moveForward);
};
this.ui.menu.onmouseenter = () => {
this.toggleMenuPersistence(true);
};
this.ui.menu.onmouseleave = () => {
this.toggleMenuPersistence(false);
};
}
addSettingsMenuEventListeners() {
this.ui.settingsMenu.imageDurationInput.onchange = () => {
this.setImageViewDuration();
if (this.currentThumb !== null && Utils.isImage(this.currentThumb)) {
this.startViewTimer(this.currentThumb);
}
};
this.ui.settingsMenu.minimumVideoDurationInput.onchange = () => {
this.setMinimumVideoViewDuration();
if (this.currentThumb !== null && !Utils.isImage(this.currentThumb)) {
this.startViewTimer(this.currentThumb);
}
};
}
/**
* @param {Boolean} value
*/
toggleMenuPersistence(value) {
this.menuIsPersistent = value;
this.ui.menu.classList.toggle("persistent", value);
}
/**
* @param {Boolean} value
*/
toggleMenuVisibility(value) {
this.menuIsVisible = value;
this.ui.menu.classList.toggle("visible", value);
}
/**
* @param {Boolean} value
*/
toggleSettingMenu(value) {
if (value === undefined) {
this.ui.settingsMenu.container.classList.toggle("visible");
this.ui.settingsButton.classList.toggle("settings-menu-opened");
} else {
this.ui.settingsMenu.container.classList.toggle("visible", value);
this.ui.settingsButton.classList.toggle("settings-menu-opened", value);
}
}
/**
* @param {Boolean} value
*/
toggle(value) {
Utils.setPreference(Autoplay.preferences.active, value);
this.active = value;
if (value) {
this.events.onEnable();
} else {
this.events.onDisable();
}
}
setImageViewDuration() {
let durationInSeconds = parseFloat(this.ui.settingsMenu.imageDurationInput.value);
if (isNaN(durationInSeconds)) {
durationInSeconds = Autoplay.settings.imageViewDurationInSeconds;
}
const duration = Math.round(Utils.clamp(durationInSeconds * 1000, 1000, 60000));
Utils.setPreference(Autoplay.preferences.imageDuration, duration);
Autoplay.settings.imageViewDuration = duration;
this.imageViewTimer.waitTime = duration;
this.ui.settingsMenu.imageDurationInput.value = Autoplay.settings.imageViewDurationInSeconds;
this.insertImageProgressHTML();
}
setMinimumVideoViewDuration() {
let durationInSeconds = parseFloat(this.ui.settingsMenu.minimumVideoDurationInput.value);
if (isNaN(durationInSeconds)) {
durationInSeconds = Autoplay.settings.minimumVideoDurationInSeconds;
}
const duration = Math.round(Utils.clamp(durationInSeconds * 1000, 0, 60000));
Utils.setPreference(Autoplay.preferences.minimumVideoDuration, duration);
Autoplay.settings.minimumVideoDuration = duration;
this.videoViewTimer.waitTime = duration;
this.ui.settingsMenu.minimumVideoDurationInput.value = Autoplay.settings.minimumVideoDurationInSeconds;
this.insertVideoProgressHTML();
}
/**
* @param {HTMLElement} thumb
*/
startViewTimer(thumb) {
if (thumb === null) {
return;
}
this.currentThumb = thumb;
if (!this.active || Autoplay.disabled || this.paused) {
return;
}
if (Utils.isVideo(thumb)) {
this.startVideoViewTimer();
} else {
this.startImageViewTimer();
}
}
startImageViewTimer() {
this.stopVideoProgressBar();
this.stopVideoViewTimer();
this.startImageProgressBar();
this.imageViewTimer.restart();
}
stopImageViewTimer() {
this.imageViewTimer.stop();
this.stopImageProgressBar();
}
startVideoViewTimer() {
this.stopImageViewTimer();
this.stopImageProgressBar();
this.startVideoProgressBar();
this.videoViewTimer.restart();
}
stopVideoViewTimer() {
this.videoViewTimer.stop();
this.stopVideoProgressBar();
}
/**
* @param {HTMLElement} thumb
*/
start(thumb) {
if (!this.active || Autoplay.disabled) {
return;
}
this.addAutoplayEventListeners();
this.ui.container.style.visibility = "visible";
this.showMenu();
this.startViewTimer(thumb);
}
stop() {
if (Autoplay.disabled) {
return;
}
this.ui.container.style.visibility = "hidden";
this.removeAutoplayEventListeners();
this.stopImageViewTimer();
this.stopVideoViewTimer();
this.forceHideMenu();
}
pause() {
this.paused = !this.paused;
Utils.setPreference(Autoplay.preferences.paused, this.paused);
if (this.paused) {
this.ui.playButton.src = Autoplay.menuIconImageURLs.play;
this.ui.playButton.title = "Resume Autoplay";
this.stopImageViewTimer();
this.stopVideoViewTimer();
this.events.onPause();
} else {
this.ui.playButton.src = Autoplay.menuIconImageURLs.pause;
this.ui.playButton.title = "Pause Autoplay";
this.startViewTimer(this.currentThumb);
this.events.onResume();
}
}
onVideoEnded() {
if (this.videoViewTimer.timeout === null) {
this.events.onComplete();
} else {
this.events.onVideoEndedBeforeMinimumViewTime();
}
}
addAutoplayEventListeners() {
this.imageViewTimer.onCooldownEnd = () => {
this.events.onComplete();
};
document.addEventListener("mousemove", () => {
this.showMenu();
}, {
signal: this.eventListenersAbortController.signal
});
document.addEventListener("keydown", (event) => {
if (!Utils.isHotkeyEvent(event)) {
return;
}
switch (event.key.toLowerCase()) {
case "p":
this.showMenu();
this.pause();
break;
default:
break;
}
}, {
signal: this.eventListenersAbortController.signal
});
}
removeAutoplayEventListeners() {
this.imageViewTimer.onCooldownEnd = () => { };
this.eventListenersAbortController.abort();
this.eventListenersAbortController = new AbortController();
}
showMenu() {
this.toggleMenuVisibility(true);
this.menuVisibilityTimer.restart();
}
hideMenu() {
this.toggleMenuVisibility(false);
}
forceHideMenu() {
this.toggleMenuPersistence(false);
this.toggleMenuVisibility(false);
this.toggleSettingMenu(false);
}
startImageProgressBar() {
this.stopImageProgressBar();
setTimeout(() => {
this.ui.imageProgressBar.classList.add("animated");
}, 10);
}
stopImageProgressBar() {
this.ui.imageProgressBar.classList.remove("animated");
}
startVideoProgressBar() {
this.stopVideoProgressBar();
setTimeout(() => {
this.ui.videoProgressBar.classList.add("animated");
}, 10);
}
stopVideoProgressBar() {
this.ui.videoProgressBar.classList.remove("animated");
}
}
class Gallery {
static 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%);
}
#gallery-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 {
cursor: default;
video {
top: 0;
left: 0;
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;
}
.favorite,
.thumb {
>div,
>a {
>canvas {
width: 100%;
position: absolute;
top: 0;
left: 0;
pointer-events: none;
z-index: 1;
}
}
}
#original-content-background {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: black;
z-index: 999;
display: none;
pointer-events: none;
cursor: default;
-webkit-user-drag: none;
-khtml-user-drag: none;
-moz-user-drag: none;
-o-user-drag: none;
}
</style>
`;
static galleryDebugHTML = `
.thumb,
.favorite {
&.debug-selected {
outline: 3px solid #0075FF !important;
}
&.loaded {
div, a {
outline: 2px solid transparent;
animation: outlineGlow 1s forwards;
}
.image {
opacity: 1;
}
}
>a
>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;
}
}
`;
static directions = {
d: "d",
a: "a",
right: "ArrowRight",
left: "ArrowLeft"
};
static preferences = {
showOnHover: "showImagesWhenHovering",
backgroundOpacity: "galleryBackgroundOpacity",
resolution: "galleryResolution",
enlargeOnClick: "enlargeOnClick",
videoVolume: "videoVolume",
videoMuted: "videoMuted"
};
static webWorkers = {
renderer:
`
/* eslint-disable prefer-template */
/**
* @param {Number} milliseconds
* @returns {Promise}
*/
function sleep(milliseconds) {
return new Promise(resolve => setTimeout(resolve, milliseconds));
}
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 {Number}
*/
get estimatedMegabyteSize() {
const rgb = 3;
const bytes = rgb * this.pixelCount;
const numberOfBytesInMegabyte = 1048576;
return bytes / numberOfBytesInMegabyte;
}
/**
* @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();
}
}
class BatchRenderRequest {
static settings = {
megabyteMemoryLimit: 1000,
minimumRequestCount: 10
};
/**
* @type {String}
*/
id;
/**
* @type {String}
*/
requestType;
/**
* @type {RenderRequest[]}
*/
renderRequests;
/**
* @type {RenderRequest[]}
*/
originalRenderRequests;
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.originalRenderRequests = this.renderRequests;
this.truncateRenderRequestsExceedingMemoryLimit();
}
truncateRenderRequestsExceedingMemoryLimit() {
const truncatedRequests = [];
let currentMegabyteSize = 0;
for (const request of this.renderRequests) {
const overMemoryLimit = currentMegabyteSize < BatchRenderRequest.settings.megabyteMemoryLimit;
const underMinimumRequestCount = truncatedRequests.length < BatchRenderRequest.settings.minimumRequestCount;
if (overMemoryLimit || underMinimumRequestCount) {
truncatedRequests.push(request);
currentMegabyteSize += request.estimatedMegabyteSize;
} else {
postMessage({
action: "renderDeleted",
id: request.id
});
}
}
this.renderRequests = truncatedRequests;
}
}
class ImageFetcher {
/**
* @type {Set.<String>}
*/
static idsToFetchFromPostPages = new Set();
/**
* @type {Number}
*/
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")) {
return ImageFetcher.getOriginalImageURLFromPostPage(id);
}
console.error({
error,
url: postPageURL
});
return "https://rule34.xxx/images/r34chibi.png";
});
}
/**
* @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.upscale(request, imageBitmap);
});
});
await sleep(50);
}
}
/**
* @param {RenderRequest} request
* @param {ImageBitmap} imageBitmap
*/
upscale(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.originalRenderRequests.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;
/**
* @type {Boolean}
*/
get hasRenderRequest() {
return this.renderRequest !== undefined &&
this.renderRequest !== null;
}
/**
* @type {Boolean}
*/
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;
this.configureCanvasQuality();
}
configureCanvasQuality() {
this.context.imageSmoothingEnabled = true;
this.context.imageSmoothingQuality = "high";
this.context.lineJoin = "miter";
}
renderMultipleImages(message) {
const batchRenderRequest = new BatchRenderRequest(message);
this.thumbUpscaler.collectCanvases(batchRenderRequest);
this.abortOutdatedFetchRequests(batchRenderRequest);
this.deleteRendersNotInNewRequests(batchRenderRequest);
this.removeStartedRenderRequests(batchRenderRequest);
this.batchRenderRequest = batchRenderRequest;
this.renderMultipleImagesHelper(batchRenderRequest);
}
/**
* @param {BatchRenderRequest} batchRenderRequest
*/
async renderMultipleImagesHelper(batchRenderRequest) {
for (const request of batchRenderRequest.renderRequests) {
if (this.renders.has(request.id)) {
continue;
}
this.renders.set(request.id, {
completed: false,
imageBitmap: undefined,
request
});
}
for (const request of batchRenderRequest.renderRequests) {
this.renderImage(request);
await sleep(request.fetchDelay);
}
}
/**
* @param {RenderRequest} request
* @param {Number} batchRequestId
*/
async renderImage(request) {
this.incompleteRenderRequests.set(request.id, request);
await ImageFetcher.setOriginalImageURLAndExtension(request);
let blob;
try {
blob = await ImageFetcher.fetchImageBlob(request);
} catch (error) {
if (error.name === "AbortError") {
this.deleteRender(request.id);
} else {
console.error({
error,
request
});
}
return;
}
const imageBitmap = await createImageBitmap(blob);
this.renders.set(request.id, {
completed: true,
imageBitmap,
request
});
this.incompleteRenderRequests.delete(request.id);
this.thumbUpscaler.upscale(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}
*/
renderHasCompleted(id) {
const render = this.renders.get(id);
return render !== undefined && render.completed;
}
/**
* @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
*/
deleteRendersNotInNewRequests(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} batchRenderRequest
*/
removeStartedRenderRequests(batchRenderRequest) {
batchRenderRequest.renderRequests = batchRenderRequest.renderRequests
.filter(request => !this.renders.has(request.id));
}
/**
* @param {BatchRenderRequest} batchRenderRequest
*/
removeCompletedRenderRequests(batchRenderRequest) {
batchRenderRequest.renderRequests = batchRenderRequest.renderRequests
.filter(request => !this.renderHasCompleted(request.id));
}
upscaleAllRenderedThumbs() {
for (const render of this.renders.values()) {
this.thumbUpscaler.upscale(render.request, render.imageBitmap);
}
}
onmessage(message) {
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":
this.renderMultipleImages(message);
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;
case "upscaleAllRenderedThumbs":
this.upscaleAllRenderedThumbs();
break;
default:
break;
}
}
}
/**
* @type {ImageRenderer}
*/
let imageRenderer;
onmessage = (message) => {
switch (message.data.action) {
case "initialize":
BatchRenderRequest.settings.megabyteMemoryLimit = message.data.megabyteLimit;
BatchRenderRequest.settings.minimumRequestCount = message.data.minimumImagesToRender;
imageRenderer = new ImageRenderer(message.data);
break;
case "findExtension":
ImageFetcher.findImageExtensionFromId(message.data.id);
break;
default:
imageRenderer.onmessage(message.data);
break;
}
};
`
};
static mainCanvasResolutions = {
search: Utils.onMobileDevice() ? "7680x4320" : "3840x2160",
favorites: "7680x4320"
};
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: Utils.onMobileDevice() ? 2 : 50,
megabyteLimit: Utils.onMobileDevice() ? 0 : 400,
minImagesToRender: Utils.onMobileDevice() ? 3 : 8,
imageFetchDelay: 250,
throttledImageFetchDelay: 400,
imageFetchDelayWhenExtensionKnown: 25,
upscaledThumbResolutionFraction: 4,
upscaledAnimatedThumbResolutionFraction: 6,
animatedThumbsToUpscaleRange: 20,
animatedThumbsToUpscaleDiscrete: 20,
traversalCooldownTime: 300,
renderOnPageChangeCooldownTime: 2000,
addFavoriteCooldownTime: 250,
cursorVisibilityCooldownTime: 500,
imageExtensionAssignmentCooldownTime: 1000,
additionalVideoPlayerCount: Utils.onMobileDevice() ? 0 : 2,
renderAroundAggressively: true,
loopAtEndOfGalleryValue: false,
get loopAtEndOfGallery() {
if (!Utils.onFavoritesPage() || !Gallery.finishedLoading) {
return true;
}
return this.loopAtEndOfGalleryValue;
},
debugEnabled: false
};
static keyHeldDownTraversalCooldown = new Cooldown(Gallery.settings.traversalCooldownTime);
static backgroundRenderingOnPageChangeCooldown = new Cooldown(Gallery.settings.renderOnPageChangeCooldownTime, true);
static addOrRemoveFavoriteCooldown = new Cooldown(Gallery.settings.addFavoriteCooldownTime, true);
static cursorVisibilityCooldown = new Cooldown(Gallery.settings.cursorVisibilityCooldownTime);
static finishedLoading = Utils.onSearchPage();
/**
* @returns {Boolean}
*/
static get disabled() {
return (Utils.onMobileDevice() && Utils.onSearchPage()) || Utils.getPerformanceProfile() > 0 || Utils.onPostPage();
}
/**
* @type {Autoplay}
*/
autoplayController;
/**
* @type {HTMLDivElement}
*/
originalContentContainer;
/**
* @type {HTMLCanvasElement}
*/
mainCanvas;
/**
* @type {HTMLCanvasElement}
*/
lowResolutionCanvas;
/**
* @type {CanvasRenderingContext2D}
*/
lowResolutionContext;
/**
* @type {HTMLAnchorElement}
*/
videoContainer;
/**
* @type {HTMLVideoElement[]}
*/
videoPlayers;
/**
* @type {HTMLImageElement}
*/
gifContainer;
/**
* @type {HTMLAnchorElement}
*/
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 {Map.<String, String>}
*/
enumeratedThumbs;
/**
* @type {HTMLElement[]}
*/
visibleThumbs;
/**
* @type {Post[]}
*/
latestSearchResults;
/**
* @type {Object.<Number, String>}
*/
imageExtensions;
/**
* @type {String}
*/
foundFavoriteId;
/**
* @type {String}
*/
changedPageInGalleryDirection;
/**
* @type {Number}
*/
recentlyDiscoveredImageExtensionCount;
/**
* @type {Number}
*/
currentlySelectedThumbIndex;
/**
* @type {Number}
*/
lastSelectedThumbIndexBeforeEnteringGallery;
/**
* @type {Number}
*/
currentBatchRenderRequestId;
/**
* @type {Boolean}
*/
inGallery;
/**
* @type {Boolean}
*/
recentlyEnteredGallery;
/**
* @type {Boolean}
*/
recentlyExitedGallery;
/**
* @type {Boolean}
*/
leftPage;
/**
* @type {Boolean}
*/
favoritesWereFetched;
/**
* @type {Boolean}
*/
showOriginalContentOnHover;
/**
* @type {Boolean}
*/
enlargeOnClickOnMobile;
/**
* @type {Boolean}
*/
get changedPageWhileInGallery() {
return this.changedPageInGalleryDirection !== null;
}
constructor() {
if (Gallery.disabled) {
return;
}
this.createAutoplayController();
this.initializeFields();
this.initializeTimers();
this.setMainCanvasResolution();
this.createWebWorkers();
this.createVideoBackgrounds();
this.addEventListeners();
this.createImageRendererMessageHandler();
this.prepareSearchPage();
this.insertHTML();
this.updateBackgroundOpacity(Utils.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.enumeratedThumbs = new Map();
this.visibleThumbs = [];
this.latestSearchResults = [];
this.imageExtensions = {};
this.foundFavoriteId = null;
this.changedPageInGalleryDirection = null;
this.recentlyDiscoveredImageExtensionCount = 0;
this.currentlySelectedThumbIndex = 0;
this.lastSelectedThumbIndexBeforeEnteringGallery = 0;
this.currentBatchRenderRequestId = 0;
this.inGallery = false;
this.recentlyEnteredGallery = false;
this.recentlyExitedGallery = false;
this.leftPage = false;
this.favoritesWereFetched = false;
this.showOriginalContentOnHover = Utils.getPreference(Gallery.preferences.showOnHover, true);
this.enlargeOnClickOnMobile = Utils.getPreference(Gallery.preferences.enlargeOnClick, true);
}
initializeTimers() {
Gallery.backgroundRenderingOnPageChangeCooldown.onDebounceEnd = () => {
this.onPageChange();
};
}
setMainCanvasResolution() {
const resolution = Utils.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(Utils.getWorkerURL(Gallery.webWorkers.renderer));
this.imageRenderer.postMessage({
action: "initialize",
canvas: offscreenCanvas,
onMobileDevice: Utils.onMobileDevice(),
screenWidth: window.screen.width,
megabyteLimit: Gallery.settings.megabyteLimit,
minimumImagesToRender: Gallery.settings.minImagesToRender,
onSearchPage: Utils.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.addMobileEventListeners();
this.addMemoryManagementEventListeners();
}
addGalleryEventListeners() {
window.addEventListener("load", () => {
if (Utils.onSearchPage()) {
this.initializeThumbsForHovering.bind(this)();
this.enumerateThumbs();
}
this.hideCaptionsWhenShowingOriginalContent();
}, {
once: true,
passive: true
});
// eslint-disable-next-line complexity
document.addEventListener("mousedown", (event) => {
const autoplayMenu = document.getElementById("autoplay-menu");
if (autoplayMenu !== null && autoplayMenu.contains(event.target)) {
return;
}
const clickedOnAnImage = event.target.tagName.toLowerCase() === "img" && !event.target.parentElement.classList.contains("add-or-remove-button");
const clickedOnAThumb = clickedOnAnImage && (Utils.getThumbFromImage(event.target).className.includes("thumb") || Utils.getThumbFromImage(event.target).className.includes(Utils.favoriteItemClassName));
const clickedOnACaptionTag = event.target.classList.contains("caption-tag");
const thumb = clickedOnAThumb ? Utils.getThumbFromImage(event.target) : null;
if (clickedOnAThumb) {
this.currentlySelectedThumbIndex = this.getIndexFromThumb(thumb);
}
if (event.ctrlKey && event.button === Utils.clickCodes.left) {
return;
}
switch (event.button) {
case Utils.clickCodes.left:
if (this.inGallery) {
if (Utils.isVideo(this.getSelectedThumb()) && !Utils.onMobileDevice()) {
return;
}
this.exitGallery();
this.toggleAllVisibility(false);
return;
}
if (thumb === null) {
return;
}
if (Utils.onMobileDevice()) {
if (!this.enlargeOnClickOnMobile) {
this.openPostInNewPage(thumb);
return;
}
this.deleteAllRenders();
}
this.toggleAllVisibility(true);
this.enterGallery();
this.showOriginalContent(thumb);
break;
case Utils.clickCodes.middle:
event.preventDefault();
if (this.inGallery) {
this.openPostInNewPage();
return;
}
if (clickedOnAThumb && Utils.onSearchPage()) {
this.openPostInNewPage();
return;
}
if (!clickedOnAThumb && !clickedOnACaptionTag) {
this.toggleAllVisibility();
Utils.setPreference(Gallery.preferences.showOnHover, this.showOriginalContentOnHover);
}
break;
default:
break;
}
});
window.addEventListener("auxclick", (event) => {
if (event.button === Utils.clickCodes.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(Utils.getPreference(Gallery.preferences.backgroundOpacity, 1));
opacity -= event.deltaY * 0.0005;
opacity = Utils.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;
case " ":
if (Utils.isVideo(this.getSelectedThumb())) {
const video = this.getActiveVideoPlayer();
if (video === document.activeElement) {
return;
}
if (video.paused) {
video.play().catch(() => { });
} else {
video.pause();
}
}
break;
default:
break;
}
}, {
passive: true
});
window.addEventListener("keydown", async(event) => {
if (!this.inGallery) {
return;
}
const zoomedIn = document.getElementById("main-canvas-zoom") !== null;
switch (event.key) {
case "F":
case "f":
await this.addFavoriteInGallery(event);
break;
case "M":
case "m":
if (Utils.isVideo(this.getSelectedThumb())) {
this.getActiveVideoPlayer().muted = !this.getActiveVideoPlayer().muted;
}
break;
case "B":
case "b":
this.toggleBackgroundOpacity();
break;
case "n":
this.toggleCursorVisibility(true);
Gallery.cursorVisibilityCooldown.restart();
break;
case "Escape":
this.exitGallery();
this.toggleAllVisibility(false);
break;
case "Control":
this.setGalleryCursor(zoomedIn ? "zoom-out" : "zoom-in");
break;
default:
break;
}
}, {
passive: true
});
window.addEventListener("keyup", (event) => {
if (!this.inGallery) {
return;
}
switch (event.key) {
case "Control":
this.setGalleryCursor("default");
break;
default:
break;
}
});
}
addFavoritesLoaderEventListeners() {
if (Utils.onSearchPage()) {
return;
}
window.addEventListener("favoritesFetched", () => {
this.initializeThumbsForHovering.bind(this)();
this.enumerateThumbs();
});
window.addEventListener("newFavoritesFetchedOnReload", (event) => {
if (event.detail.empty) {
return;
}
this.initializeThumbsForHovering.bind(this)(event.detail.thumbs);
this.enumerateThumbs();
/**
* @type {HTMLElement[]}
*/
const thumbs = event.detail.thumbs.reverse();
if (thumbs.length > 0) {
const thumb = thumbs[0];
this.upscaleAnimatedThumbsAround(thumb);
this.renderImages(thumbs
.filter(t => Utils.isImage(t))
.slice(0, 20));
}
}, {
once: true
});
window.addEventListener("startedFetchingFavorites", () => {
this.favoritesWereFetched = true;
setTimeout(() => {
const thumb = document.querySelector(`.${Utils.favoriteItemClassName}`);
this.renderImagesInTheBackground();
if (thumb !== null && !Gallery.finishedLoading) {
this.upscaleAnimatedThumbsAround(thumb);
}
}, 650);
}, {
once: true
});
window.addEventListener("favoritesLoaded", () => {
Gallery.backgroundRenderingOnPageChangeCooldown.waitTime = 1000;
Gallery.finishedLoading = true;
this.initializeThumbsForHovering.bind(this)();
this.enumerateThumbs();
this.findImageExtensionsInTheBackground();
if (!this.favoritesWereFetched) {
this.renderImagesInTheBackground();
}
}, {
once: true
});
window.addEventListener("newSearchResults", (event) => {
this.latestSearchResults = event.detail;
});
window.addEventListener("changedPage", () => {
this.initializeThumbsForHovering.bind(this)();
this.enumerateThumbs();
if (this.changedPageWhileInGallery) {
setTimeout(() => {
this.imageRenderer.postMessage({
action: "upscaleAllRenderedThumbs"
});
}, 100);
} else {
this.clearMainCanvas();
this.clearVideoSources();
this.toggleOriginalContentVisibility(false);
this.deleteAllRenders();
if (Gallery.settings.debugEnabled) {
Utils.getAllThumbs().forEach((thumb) => {
thumb.classList.remove("loaded");
thumb.classList.remove("debug-selected");
});
}
}
this.onPageChange();
});
window.addEventListener("foundFavorite", (event) => {
this.foundFavoriteId = event.detail;
});
window.addEventListener("shuffle", () => {
this.enumerateThumbs();
this.deleteAllRenders();
this.renderImagesInTheBackground();
});
// window.addEventListener("metadataFetched", (event) => {
// Gallery.assignImageExtension(event.detail.id, event.detail.extension);
// });
window.addEventListener("didNotChangePageInGallery", (event) => {
if (this.inGallery) {
this.setNextSelectedThumbIndex(event.detail);
this.traverseGalleryHelper();
}
});
}
createImageRendererMessageHandler() {
this.imageRenderer.onmessage = (message) => {
message = message.data;
switch (message.action) {
case "renderCompleted":
this.onRenderCompleted(message);
break;
case "renderDeleted":
this.onRenderDeleted(message);
break;
case "extensionFound":
Utils.assignImageExtension(message.id, message.extension);
break;
default:
break;
}
};
}
addMobileEventListeners() {
if (!Utils.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(false);
}
}, {
passive: false
});
window.addEventListener("orientationchange", () => {
if (this.imageRenderer !== null && this.imageRenderer !== undefined) {
this.setMainCanvasOrientation();
}
}, {
passive: true
});
}
setMainCanvasOrientation() {
if (!Utils.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 (Utils.onFavoritesPage()) {
return;
}
window.addEventListener("blur", () => {
this.leftPage = true;
this.deleteAllRenders();
this.clearInactiveVideoSources();
});
window.addEventListener("focus", () => {
if (this.leftPage) {
this.renderImagesInTheBackground();
this.leftPage = false;
}
});
}
async prepareSearchPage() {
if (!Utils.onSearchPage()) {
return;
}
await Utils.findImageExtensionsOnSearchPage();
dispatchEvent(new Event("foundExtensionsOnSearchPage"));
this.renderImagesInTheBackground();
}
insertHTML() {
this.insertStyleHTML();
this.insertDebugHTML();
this.insertOptionsHTML();
this.insertOriginalContentContainerHTML();
}
insertStyleHTML() {
Utils.insertStyleHTML(Gallery.galleryHTML, "gallery");
}
insertDebugHTML() {
if (Gallery.settings.debugEnabled) {
Utils.insertStyleHTML(Gallery.galleryDebugHTML, "gallery-debug");
}
}
insertOptionsHTML() {
this.insertShowOnHoverOption();
}
insertShowOnHoverOption() {
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) => {
Utils.setPreference(Gallery.preferences.showOnHover, event.target.checked);
this.toggleAllVisibility(event.target.checked);
};
if (Utils.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) => {
Utils.setPreference(Gallery.preferences.enlargeOnClick, event.target.checked);
this.enlargeOnClickOnMobile = event.target.checked;
};
}
Utils.createFavoritesOption(
optionId,
optionText,
optionTitle,
optionIsChecked,
onOptionChanged,
true
// "(Middle Click)"
);
}
insertOriginalContentContainerHTML() {
const originalContentContainerHTML = `
<div id="gallery-container">
<a id="original-video-container">
<video id="video-player-0" width="100%" height="100%" autoplay muted loop controlsList="nofullscreen" active></video>
</a>
<img id="original-gif-container" class="focused"></img>
<a id="original-content-background"></a>
</div>
`;
Utils.insertFavoritesSearchGalleryHTML("afterbegin", originalContentContainerHTML);
this.originalContentContainer = document.getElementById("gallery-container");
this.originalContentContainer.insertBefore(this.lowResolutionCanvas, this.originalContentContainer.firstChild);
this.originalContentContainer.insertBefore(this.mainCanvas, this.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.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);
this.addBackgroundEventListeners();
if (Autoplay.disabled || !this.autoplayController.active || this.autoplayController.paused) {
this.toggleVideoLooping(true);
} else {
this.toggleVideoLooping(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() {
this.videoContainer.onclick = (event) => {
if (!event.ctrlKey) {
event.preventDefault();
}
};
for (const video of this.videoPlayers) {
video.addEventListener("mousemove", () => {
if (!video.hasAttribute("controls")) {
video.setAttribute("controls", "");
}
}, {
passive: true
});
video.addEventListener("click", (event) => {
if (event.ctrlKey) {
return;
}
if (video.paused) {
video.play().catch(() => { });
} else {
video.pause();
}
}, {
passive: true
});
video.addEventListener("volumechange", (event) => {
if (!event.target.hasAttribute("active")) {
return;
}
Utils.setPreference(Gallery.preferences.videoVolume, video.volume);
Utils.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.autoplayController.onVideoEnded();
}, {
passive: true
});
video.addEventListener("dblclick", () => {
if (this.inGallery && !this.recentlyEnteredGallery) {
this.exitGallery();
this.toggleAllVisibility(false);
}
});
}
}
addBackgroundEventListeners() {
if (Utils.onMobileDevice()) {
return;
}
this.background.addEventListener("mousemove", () => {
Gallery.cursorVisibilityCooldown.restart();
this.toggleCursorVisibility(true);
}, {
passive: true
});
Gallery.cursorVisibilityCooldown.onCooldownEnd = () => {
if (this.inGallery) {
this.toggleCursorVisibility(false);
}
};
}
loadVideoVolume() {
const video = this.getActiveVideoPlayer();
video.volume = parseFloat(Utils.getPreference(Gallery.preferences.videoVolume, 1));
video.muted = Utils.getPreference(Gallery.preferences.videoMuted, true);
}
/**
* @param {Number} opacity
*/
updateBackgroundOpacity(opacity) {
this.background.style.opacity = opacity;
Utils.setPreference(Gallery.preferences.backgroundOpacity, opacity);
}
createAutoplayController() {
const subscribers = new AutoplayListenerList(
() => {
this.toggleVideoLooping(false);
},
() => {
this.toggleVideoLooping(true);
},
() => {
this.toggleVideoLooping(true);
},
() => {
this.toggleVideoLooping(false);
},
() => {
if (this.inGallery) {
const direction = Autoplay.settings.moveForward ? Gallery.directions.right : Gallery.directions.left;
this.traverseGallery(direction, false);
}
},
() => {
if (this.inGallery && Utils.isVideo(this.getSelectedThumb())) {
this.playOriginalVideo(this.getSelectedThumb());
}
}
);
this.autoplayController = new Autoplay(subscribers);
}
/**
* @param {HTMLElement[]} thumbs
*/
initializeThumbsForHovering(thumbs) {
const thumbElements = thumbs === undefined ? Utils.getAllThumbs() : thumbs;
for (const thumbElement of thumbElements) {
this.addEventListenersToThumb(thumbElement);
}
}
renderImagesInTheBackground() {
if (Utils.onMobileDevice() && !this.enlargeOnClickOnMobile) {
return;
}
const animatedThumbsToUpscale = Utils.getAllThumbs()
.slice(0, Gallery.settings.animatedThumbsToUpscaleDiscrete)
.filter(thumb => !Utils.isImage(thumb));
this.upscaleAnimatedThumbs(animatedThumbsToUpscale);
const imageThumbsToRender = this.getUnrenderedImageThumbs()
.slice(0, Gallery.settings.maxImagesToRenderInBackground);
this.renderImages(imageThumbsToRender);
}
onPageChange() {
this.onPageChangeHelper();
this.foundFavoriteId = null;
this.changedPageInGalleryDirection = null;
}
onPageChangeHelper() {
if (this.visibleThumbs.length <= 0) {
return;
}
if (this.changedPageInGalleryDirection !== null) {
this.onPageChangedInGallery();
return;
}
if (this.foundFavoriteId !== null) {
this.onFavoriteFound();
return;
}
setTimeout(() => {
if (Gallery.backgroundRenderingOnPageChangeCooldown.ready) {
this.renderImagesInTheBackground();
}
}, 100);
}
onPageChangedInGallery() {
if (this.changedPageInGalleryDirection === "ArrowRight") {
this.currentlySelectedThumbIndex = 0;
} else {
this.currentlySelectedThumbIndex = this.visibleThumbs.length - 1;
}
this.traverseGalleryHelper();
}
onFavoriteFound() {
const thumb = document.getElementById(this.foundFavoriteId);
if (thumb !== null) {
this.renderImagesAround(thumb);
}
}
/**
* @param {HTMLElement[]} imagesToRender
*/
renderImages(imagesToRender) {
const renderRequests = imagesToRender.map(image => this.getRenderRequest(image));
const canvases = Utils.onSearchPage() ? [] : renderRequests
.filter(request => request.canvas !== undefined)
.map(request => request.canvas);
this.imageRenderer.postMessage({
action: "renderMultiple",
id: this.currentBatchRenderRequestId,
renderRequests,
requestType: "none"
}, canvases);
this.currentBatchRenderRequestId += 1;
if (this.currentBatchRenderRequestId >= 1000) {
this.currentBatchRenderRequestId = 0;
}
}
/**
* @param {Object} message
*/
onRenderCompleted(message) {
const thumb = document.getElementById(message.id);
this.completedRenders.add(message.id);
if (Gallery.settings.debugEnabled) {
if (Gallery.settings.loopAtEndOfGallery) {
if (thumb !== null) {
thumb.classList.add("loaded");
}
} else {
const post = Post.allPosts.get(message.id);
if (post !== undefined && post.root !== undefined) {
post.root.classList.add("loaded");
}
}
}
if (thumb !== null && message.extension === "gif") {
Utils.getImageFromThumb(thumb).setAttribute("gif", true);
return;
}
Utils.assignImageExtension(message.id, message.extension);
this.drawMainCanvasOnRenderCompleted(thumb);
}
/**
* @param {HTMLElement} thumb
*/
drawMainCanvasOnRenderCompleted(thumb) {
if (thumb === null) {
return;
}
const mainCanvasIsVisible = this.showOriginalContentOnHover || this.inGallery;
if (!mainCanvasIsVisible) {
return;
}
const selectedThumb = this.getSelectedThumb();
const selectedThumbIsImage = selectedThumb !== undefined && Utils.isImage(selectedThumb);
if (!selectedThumbIsImage) {
return;
}
if (selectedThumb.id === thumb.id) {
this.drawMainCanvas(thumb);
}
}
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) {
if (Gallery.settings.loopAtEndOfGallery) {
for (const thumb of this.visibleThumbs) {
thumb.classList.remove("loaded");
}
} else {
for (const post of Post.allPosts.values()) {
if (post.root !== undefined) {
post.root.classList.remove("loaded");
}
}
}
}
}
deleteAllTransferredCanvases() {
if (Utils.onSearchPage()) {
return;
}
for (const id of this.transferredCanvases.keys()) {
this.transferredCanvases.get(id).remove();
this.transferredCanvases.delete(id);
}
this.transferredCanvases.clear();
}
/**
* @returns {HTMLElement[]}
*/
getUnrenderedImageThumbs() {
const thumbs = Utils.getAllThumbs().filter((thumb) => {
return Utils.isImage(thumb) && !this.renderHasStarted(thumb);
});
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");
}
}
}
async findImageExtensionsInTheBackground() {
await Utils.sleep(1000);
const idsWithUnknownExtensions = this.getIdsWithUnknownExtensions(Array.from(Post.allPosts.values()));
while (idsWithUnknownExtensions.length > 0) {
await Utils.sleep(3000);
while (idsWithUnknownExtensions.length > 0 && Gallery.finishedLoading) {
const id = idsWithUnknownExtensions.pop();
if (id !== undefined && id !== null && !Utils.extensionIsKnown(id)) {
this.imageRenderer.postMessage({
action: "findExtension",
id
});
await Utils.sleep(10);
}
}
}
Gallery.settings.extensionsFoundBeforeSavingCount = 0;
}
enumerateThumbs() {
this.visibleThumbs = Utils.getAllThumbs();
this.enumeratedThumbs.clear();
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) {
this.enumeratedThumbs.set(thumb.id, index);
}
/**
* @param {HTMLElement} thumb
* @returns {Number | null}
*/
getIndexFromThumb(thumb) {
return this.enumeratedThumbs.get(thumb.id) || 0;
}
/**
* @param {HTMLElement} thumb
*/
addEventListenersToThumb(thumb) {
if (Utils.onMobileDevice()) {
return;
}
const image = Utils.getImageFromThumb(thumb);
if (image.onmouseover !== null) {
return;
}
image.onmouseover = (event) => {
if (this.inGallery || this.recentlyExitedGallery || Utils.enteredOverCaptionTag(event)) {
return;
}
this.thumbUnderCursor = thumb;
this.lastEnteredThumb = thumb;
this.showOriginalContent(thumb);
};
image.onmouseout = (event) => {
this.thumbUnderCursor = null;
if (this.inGallery || Utils.enteredOverCaptionTag(event)) {
return;
}
this.stopAllVideos();
this.hideOriginalContent();
};
}
/**
* @param {HTMLElement} thumb
*/
openPostInNewPage(thumb) {
thumb = thumb === undefined || thumb === null ? this.getSelectedThumb() : thumb;
Utils.openPostInNewTab(Utils.getIdFromThumb(thumb));
}
unFavoriteSelectedContent() {
if (!Utils.userIsOnTheirOwnFavoritesPage()) {
return;
}
const selectedThumb = this.getSelectedThumb();
if (selectedThumb === null) {
return;
}
const removeFavoriteButton = Utils.getRemoveFavoriteButtonFromThumb(selectedThumb);
if (removeFavoriteButton === null) {
return;
}
const showRemoveFavoriteButtons = document.getElementById("show-remove-favorite-buttons");
if (showRemoveFavoriteButtons === null) {
return;
}
if (!Gallery.addOrRemoveFavoriteCooldown.ready) {
return;
}
if (!showRemoveFavoriteButtons.checked) {
Utils.showFullscreenIcon(Utils.icons.warning, 1000);
setTimeout(() => {
alert("The \"Remove Buttons\" option must be checked to use this hotkey");
}, 20);
return;
}
Utils.showFullscreenIcon(Utils.icons.heartMinus);
this.onFavoriteAddedOrDeleted(selectedThumb.id);
Utils.removeFavorite(selectedThumb.id);
}
enterGallery() {
const selectedThumb = this.getSelectedThumb();
this.lastSelectedThumbIndexBeforeEnteringGallery = this.currentlySelectedThumbIndex;
this.background.style.pointerEvents = "auto";
if (Utils.isVideo(selectedThumb)) {
this.toggleVideoControls(true);
}
this.inGallery = true;
dispatchEvent(new CustomEvent("showOriginalContent", {
detail: true
}));
this.autoplayController.start(selectedThumb);
Gallery.cursorVisibilityCooldown.restart();
this.recentlyEnteredGallery = true;
setTimeout(() => {
this.recentlyEnteredGallery = false;
}, 300);
this.setupOriginalImageLinkInGallery();
}
exitGallery() {
if (Gallery.settings.debugEnabled) {
Utils.getAllThumbs().forEach(thumb => thumb.classList.remove("debug-selected"));
}
this.toggleCursorVisibility(true);
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.autoplayController.stop();
this.setGalleryCursor("default");
document.dispatchEvent(new Event("mousemove"));
}
/**
* @param {String} direction
* @param {Boolean} keyIsHeldDown
*/
traverseGallery(direction, keyIsHeldDown) {
if (Gallery.settings.debugEnabled) {
this.getSelectedThumb().classList.remove("debug-selected");
}
if (keyIsHeldDown && !Gallery.keyHeldDownTraversalCooldown.ready) {
return;
}
if (!Gallery.settings.loopAtEndOfGallery && this.reachedEndOfGallery(direction) && Gallery.finishedLoading) {
this.changedPageInGalleryDirection = direction;
dispatchEvent(new CustomEvent("reachedEndOfGallery", {
detail: direction
}));
return;
}
this.setNextSelectedThumbIndex(direction);
this.traverseGalleryHelper();
}
traverseGalleryHelper() {
const selectedThumb = this.getSelectedThumb();
this.autoplayController.startViewTimer(selectedThumb);
this.clearOriginalContentSources();
this.stopAllVideos();
if (Gallery.settings.debugEnabled) {
selectedThumb.classList.add("debug-selected");
}
this.upscaleAnimatedThumbsAround(selectedThumb);
this.renderImagesAround(selectedThumb);
this.preloadInactiveVideoPlayers(selectedThumb);
if (!Utils.usingFirefox()) {
Utils.scrollToThumb(selectedThumb.id, false, true);
}
if (Utils.isVideo(selectedThumb)) {
this.toggleVideoControls(true);
this.showOriginalVideo(selectedThumb);
} else if (Utils.isGif(selectedThumb)) {
this.toggleVideoControls(false);
this.toggleOriginalVideoContainer(false);
this.showOriginalGIF(selectedThumb);
} else {
this.toggleVideoControls(false);
this.toggleOriginalVideoContainer(false);
this.showOriginalImage(selectedThumb);
}
this.setupOriginalImageLinkInGallery();
}
/**
* @param {String} direction
* @returns {Boolean}
*/
reachedEndOfGallery(direction) {
if (direction === Gallery.directions.right && this.currentlySelectedThumbIndex >= this.visibleThumbs.length - 1) {
return true;
}
if (direction === Gallery.directions.left && this.currentlySelectedThumbIndex <= 0) {
return true;
}
return false;
}
/**
* @param {String} direction
* @returns {Boolean}
*/
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;
}
return false;
}
/**
* @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
}));
Utils.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.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 Utils.isVideo(this.thumbUnderCursor);
}
/**
* @param {HTMLElement} thumb
*/
showOriginalContent(thumb) {
this.currentlySelectedThumbIndex = this.getIndexFromThumb(thumb);
this.upscaleAnimatedThumbsAroundDiscrete(thumb);
if (!this.inGallery && Gallery.settings.renderAroundAggressively) {
this.renderImagesAround(thumb);
}
if (Utils.isVideo(thumb)) {
this.showOriginalVideo(thumb);
} else if (Utils.isGif(thumb)) {
this.showOriginalGIF(thumb);
} else {
this.showOriginalImage(thumb);
}
if (this.showOriginalContentOnHover) {
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.getAdjacentVideoThumbs(initialThumb, inactiveVideoPlayers.length);
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} initialThumb
* @param {Number} limit
* @returns {HTMLElement[]}
*/
getAdjacentVideoThumbs(initialThumb, limit) {
if (Gallery.settings.loopAtEndOfGallery) {
return this.getAdjacentVideoThumbsOnCurrentPage(initialThumb, limit);
}
return this.getAdjacentVideoThumbsThroughoutAllPages(initialThumb, limit);
}
/**
* @param {HTMLElement} initialThumb
* @param {Number} limit
* @returns {HTMLElement[]}
*/
getAdjacentVideoThumbsOnCurrentPage(initialThumb, limit) {
return this.getAdjacentThumbsLooped(
initialThumb,
limit,
(t) => {
return Utils.isVideo(t) && t.id !== initialThumb.id;
}
);
}
/**
* @param {HTMLElement} initialThumb
* @param {Number} limit
* @returns {HTMLElement[]}
*/
getAdjacentVideoThumbsThroughoutAllPages(initialThumb, limit) {
return this.getAdjacentSearchResults(
initialThumb,
limit,
(t) => {
return Utils.isVideo(t) && t.id !== initialThumb.id;
}
);
}
/**
* @param {HTMLElement} thumb
* @returns {String}
*/
getVideoSource(thumb) {
return Utils.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.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");
}
/**
* @param {HTMLElement} thumb
*/
showOriginalGIF(thumb) {
const tags = Utils.getTagsFromThumb(thumb);
const extension = tags.has("animated_png") ? "png" : "gif";
const originalSource = Utils.getOriginalImageURLFromThumb(thumb).replace("jpg", extension);
this.gifContainer.src = originalSource;
if (this.showOriginalContentOnHover) {
this.gifContainer.style.visibility = "visible";
}
}
/**
* @param {HTMLElement} thumb
*/
showOriginalImage(thumb) {
if (this.renderIsCompleted(thumb)) {
this.clearLowResolutionCanvas();
this.drawMainCanvas(thumb);
} else if (this.renderHasStarted(thumb)) {
this.drawLowResolutionCanvas(thumb);
this.clearMainCanvas();
this.drawMainCanvas(thumb);
} else {
this.drawLowResolutionCanvas(thumb);
this.renderOriginalImage(thumb);
if (!this.inGallery && !Gallery.settings.renderAroundAggressively) {
this.renderImagesAround(thumb);
}
}
this.toggleOriginalContentVisibility(this.showOriginalContentOnHover);
}
/**
* @param {HTMLElement} initialThumb
*/
renderImagesAround(initialThumb) {
if (Utils.onSearchPage() || (Utils.onMobileDevice() && !this.enlargeOnClickOnMobile)) {
return;
}
this.renderImages(this.getAdjacentImageThumbs(initialThumb));
}
/**
* @param {HTMLElement} initialThumb
* @returns {HTMLElement[]}
*/
getAdjacentImageThumbs(initialThumb) {
const adjacentImageThumbs = Utils.isImage(initialThumb) ? [initialThumb] : [];
if (Gallery.settings.loopAtEndOfGallery || this.latestSearchResults.length === 0) {
return adjacentImageThumbs.concat(this.getAdjacentImageThumbsOnCurrentPage(initialThumb));
}
return adjacentImageThumbs.concat(this.getAdjacentImageThumbThroughoutAllPages(initialThumb));
}
/**
* @param {HTMLElement} initialThumb
* @returns {HTMLElement[]}
*/
getAdjacentImageThumbsOnCurrentPage(initialThumb) {
return this.getAdjacentThumbsLooped(
initialThumb,
Gallery.settings.maxImagesToRenderAround,
(thumb) => {
return Utils.isImage(thumb);
}
);
}
/**
* @param {HTMLElement} initialThumb
* @returns {HTMLElement[]}
*/
getAdjacentImageThumbThroughoutAllPages(initialThumb) {
return this.getAdjacentSearchResults(
initialThumb,
Gallery.settings.maxImagesToRenderAround,
(post) => {
return Utils.isImage(post);
}
);
}
/**
* @param {HTMLElement} initialThumb
* @param {Number} limit
* @param {Function} additionalQualifier
* @returns {HTMLElement[]}
*/
getAdjacentThumbs(initialThumb, limit, additionalQualifier) {
const adjacentThumbs = [];
let currentThumb = initialThumb;
let previousThumb = initialThumb;
let nextThumb = initialThumb;
let traverseForward = true;
while (currentThumb !== null && adjacentThumbs.length < limit) {
if (traverseForward) {
nextThumb = this.getAdjacentThumb(nextThumb, true);
} else {
previousThumb = this.getAdjacentThumb(previousThumb, false);
}
traverseForward = this.getTraversalDirection(previousThumb, traverseForward, nextThumb);
currentThumb = traverseForward ? nextThumb : previousThumb;
if (currentThumb !== null) {
if (this.isVisible(currentThumb) && additionalQualifier(currentThumb)) {
adjacentThumbs.push(currentThumb);
}
}
}
return adjacentThumbs;
}
/**
* @param {HTMLElement} initialThumb
* @param {Number} limit
* @param {Function} additionalQualifier
* @returns {HTMLElement[]}
*/
getAdjacentThumbsLooped(initialThumb, limit, additionalQualifier) {
const adjacentThumbs = [];
const discoveredIds = new Set();
let currentThumb = initialThumb;
let previousThumb = initialThumb;
let nextThumb = initialThumb;
let traverseForward = true;
while (currentThumb !== null && adjacentThumbs.length < limit) {
if (traverseForward) {
nextThumb = this.getAdjacentThumbLooped(nextThumb, true);
} else {
previousThumb = this.getAdjacentThumbLooped(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)) {
adjacentThumbs.push(currentThumb);
}
}
return adjacentThumbs;
}
/**
* @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}
*/
getAdjacentThumbLooped(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} initialThumb
* @param {Number} limit
* @param {Function} additionalQualifier
* @returns {HTMLElement[]}
*/
getAdjacentSearchResults(initialThumb, limit, additionalQualifier) {
const initialSearchResultIndex = this.latestSearchResults.findIndex(post => post.id === initialThumb.id);
if (initialSearchResultIndex === -1) {
return [];
}
const adjacentSearchResults = [];
const discoveredIds = new Set();
let currentSearchResult;
let currentIndex;
let forward = true;
let previousIndex = initialSearchResultIndex;
let nextIndex = initialSearchResultIndex;
while (adjacentSearchResults.length < limit) {
if (forward) {
nextIndex = this.getAdjacentSearchResultIndex(nextIndex, true);
currentIndex = nextIndex;
forward = false;
} else {
previousIndex = this.getAdjacentSearchResultIndex(previousIndex, false);
currentIndex = previousIndex;
forward = true;
}
currentSearchResult = this.latestSearchResults[currentIndex];
if (discoveredIds.has(currentSearchResult.id)) {
break;
}
discoveredIds.add(currentSearchResult.id);
if (additionalQualifier(currentSearchResult)) {
adjacentSearchResults.push(currentSearchResult);
}
}
for (const searchResult of adjacentSearchResults) {
searchResult.activateHTMLElement();
}
return adjacentSearchResults.map(post => post.root);
}
/**
* @param {Number} i
* @param {Boolean} forward
* @returns {Number}
*/
getAdjacentSearchResultIndex(i, forward) {
if (forward) {
i += 1;
i = i >= this.latestSearchResults.length ? 0 : i;
} else {
i -= 1;
i = i < 0 ? this.latestSearchResults.length - 1 : i;
}
return i;
}
/**
* @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}
*/
renderIsCompleted(thumb) {
return this.completedRenders.has(thumb.id);
}
/**
* @param {HTMLElement} thumb
* @returns {Boolean}
*/
canvasIsTransferrable(thumb) {
return !Utils.onMobileDevice() && !Utils.onSearchPage() && !this.transferredCanvases.has(thumb.id) && document.getElementById(thumb.id) !== null;
}
/**
* @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: Utils.getOriginalImageURLFromThumb(thumb),
id: thumb.id,
extension: Utils.getImageExtension(thumb.id),
fetchDelay: this.getBaseImageFetchDelay(thumb.id),
thumbURL: Utils.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 (Utils.onMobileDevice()) {
request.windowDimensions = {
width: window.innerWidth,
height: window.innerHeight
};
}
return request;
}
/**
* @param {HTMLElement} thumb
* @returns {Number}
*/
getPixelCount(thumb) {
if (Utils.onSearchPage()) {
return 0;
}
const defaultPixelCount = 2073600;
const pixelCount = Post.getPixelCount(thumb.id);
return pixelCount === 0 ? defaultPixelCount : pixelCount;
}
/**
* @param {HTMLElement} thumb
*/
renderOriginalImage(thumb) {
if (Utils.onSearchPage()) {
return;
}
if (this.canvasIsTransferrable(thumb)) {
const request = this.getRenderRequest(thumb);
this.imageRenderer.postMessage(request, [request.canvas]);
} else {
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 html = `
// #original-content-background {
// cursor: ${value ? "auto" : "none"};
// }
// `;
// insertStyleHTML(html, "gallery-cursor-visibility");
}
/**
* @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 (Utils.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 : this.getIndexFromThumb(this.thumbUnderCursor);
}
/**
* @returns {HTMLElement}
*/
getSelectedThumb() {
return this.visibleThumbs[this.currentlySelectedThumbIndex];
}
/**
* @param {HTMLElement[]} animatedThumbs
*/
upscaleAnimatedThumbs(animatedThumbs) {
if (Utils.onMobileDevice()) {
return;
}
const upscaleRequests = [];
for (const thumb of animatedThumbs) {
if (!this.canvasIsTransferrable(thumb)) {
continue;
}
let imageURL = Utils.getOriginalImageURL(Utils.getImageFromThumb(thumb).src);
if (Utils.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 (Utils.onFavoritesPage() && !Gallery.finishedLoading) {
return Gallery.settings.throttledImageFetchDelay;
}
if (Utils.extensionIsKnown(id)) {
return Gallery.settings.imageFetchDelayWhenExtensionKnown;
}
return Gallery.settings.imageFetchDelay;
}
/**
* @param {HTMLElement} thumb
*/
upscaleAnimatedThumbsAround(thumb) {
if (!Utils.onFavoritesPage() || Utils.onMobileDevice()) {
return;
}
const animatedThumbsToUpscale = this.getAdjacentThumbs(thumb, Gallery.settings.animatedThumbsToUpscaleRange, (t) => {
return !Utils.isImage(t) && !this.transferredCanvases.has(t.id);
});
this.upscaleAnimatedThumbs(animatedThumbsToUpscale);
}
/**
* @param {HTMLElement} thumb
*/
upscaleAnimatedThumbsAroundDiscrete(thumb) {
if (!Utils.onFavoritesPage() || Utils.onMobileDevice()) {
return;
}
const animatedThumbsToUpscale = this.getAdjacentThumbs(thumb, Gallery.settings.animatedThumbsToUpscaleDiscrete, (_) => {
return true;
}).filter(t => !Utils.isImage(t) && !this.transferredCanvases.has(t.id));
this.upscaleAnimatedThumbs(animatedThumbsToUpscale);
}
/**
* @param {Post[]} thumbs
* @returns {String[]}
*/
getIdsWithUnknownExtensions(thumbs) {
return thumbs
.filter(thumb => Utils.isImage(thumb) && !Utils.extensionIsKnown(thumb.id))
.map(thumb => thumb.id);
}
/**
* @param {String} id
*/
drawLowResolutionCanvas(thumb) {
if (Utils.onMobileDevice()) {
return;
}
const image = Utils.getImageFromThumb(thumb);
if (!Utils.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 (Utils.onMobileDevice()) {
return;
}
this.lowResolutionContext.clearRect(0, 0, this.lowResolutionCanvas.width, this.lowResolutionCanvas.height);
}
/**
* @param {Boolean} value
*/
toggleVideoLooping(value) {
for (const video of this.videoPlayers) {
video.toggleAttribute("loop", value);
}
}
loadVideoClips() {
}
/**
* @param {KeyboardEvent} event
*/
async addFavoriteInGallery(event) {
if (!this.inGallery || event.repeat || !Gallery.addOrRemoveFavoriteCooldown.ready) {
return;
}
const selectedThumb = this.getSelectedThumb();
if (selectedThumb === undefined || selectedThumb === null) {
Utils.showFullscreenIcon(Utils.icons.error);
return;
}
const addedFavoriteStatus = await Utils.addFavorite(selectedThumb.id);
let svg = Utils.icons.error;
switch (addedFavoriteStatus) {
case Utils.addedFavoriteStatuses.alreadyAdded:
svg = Utils.icons.heartCheck;
break;
case Utils.addedFavoriteStatuses.success:
svg = Utils.icons.heartPlus;
this.onFavoriteAddedOrDeleted(selectedThumb.id);
break;
default:
break;
}
Utils.showFullscreenIcon(svg);
}
/**
* @param {String} id
*/
onFavoriteAddedOrDeleted(id) {
dispatchEvent(new CustomEvent("favoriteAddedOrDeleted", {
detail: id
}));
}
/**
* @param {String} cursor
*/
setGalleryCursor(cursor) {
// this.background.style.cursor = cursor;
}
/**
* @param {MouseEvent} event
*/
zoomOnMainCanvas(event) {
const style = document.getElementById("main-canvas-zoom-fsg-style");
if (style !== null) {
style.remove();
return;
}
const x = 100 * Utils.roundToTwoDecimalPlaces(event.clientX / window.innerWidth);
const y = 100 * Utils.roundToTwoDecimalPlaces(event.clientY / window.innerHeight);
Utils.insertStyleHTML(`
#gallery-container {
canvas,
img {
transform-origin: ${x}% ${y}%;
transform: translate(-50%, -50%) scale(4) !important;
}
}
`, "main-canvas-zoom");
}
async setupOriginalImageLinkInGallery() {
const thumb = this.getSelectedThumb();
if (thumb === null || thumb === undefined) {
return;
}
const imageURL = await Utils.getOriginalImageURLWithExtension(thumb);
const container = Utils.isVideo(thumb) ? this.videoContainer : this.background;
container.href = imageURL;
}
}
class Tooltip {
static 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>
`;
/**
* @type {Boolean}
*/
static get disabled() {
return Utils.onMobileDevice() || Utils.getPerformanceProfile() > 1 || Utils.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 = Utils.getPreference("showTooltip", true);
Utils.insertFavoritesSearchGalleryHTML("afterbegin", Tooltip.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" || !Utils.isHotkeyEvent(event)) {
return;
}
if (Utils.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 (Utils.onSearchPage()) {
this.toggleVisibility();
if (this.currentImage !== null) {
this.hide();
}
}
}, {
passive: true
});
}
addSearchPageEventListeners() {
if (!Utils.onSearchPage()) {
return;
}
window.addEventListener("load", () => {
this.addEventListenersToThumbs.bind(this)();
}, {
once: true,
passive: true
});
}
addFavoritesPageEventListeners() {
if (!Utils.onFavoritesPage()) {
return;
}
window.addEventListener("favoritesFetched", () => {
this.addEventListenersToThumbs.bind(this)();
});
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 (Utils.usingDarkTheme()) {
this.tooltip.classList.remove("light-green-gradient");
this.tooltip.classList.add("dark-green-gradient");
}
}
assignColorsToMatchedTags() {
if (Utils.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 ? Utils.getAllThumbs() : thumbs;
for (const thumb of thumbs) {
const image = Utils.getImageFromThumb(thumb);
if (image.onmouseenter !== null) {
continue;
}
image.onmouseenter = (event) => {
if (Utils.enteredOverCaptionTag(event)) {
return;
}
this.currentImage = image;
if (this.visible) {
this.show(image);
}
};
image.onmouseleave = (event) => {
if (!Utils.enteredOverCaptionTag(event)) {
this.currentImage = null;
this.hide();
}
};
}
}
/**
* @param {HTMLImageElement} image
*/
setPosition(image) {
const fancyHoveringStyle = document.getElementById("fancy-image-hovering-fsg-style");
const imageChangesSizeOnHover = fancyHoveringStyle !== null && fancyHoveringStyle.textContent !== "";
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 menu = document.getElementById("favorites-search-gallery-menu");
const elementAboveTooltip = menu === null ? document.getElementById("header") : menu;
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 {HTMLImageElement} image
*/
show(image) {
this.tooltip.innerHTML = this.formatHTML(this.getTags(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}
*/
getTags(image) {
const thumb = Utils.getThumbFromImage(image);
const tags = Utils.getTagsFromThumb(thumb);
if (this.searchTagColorCodes[thumb.id] === undefined) {
tags.delete(thumb.id);
}
return Array.from(tags).sort().join(" ");
}
/**
* @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} tags
*/
formatHTML(tags) {
let unmatchedTagsHTML = "";
let matchedTagsHTML = "";
const tagList = Utils.removeExtraWhiteSpace(tags).split(" ");
for (let i = 0; i < tagList.length; i += 1) {
const tag = tagList[i];
const tagColor = this.getColorCode(tag);
const tagWithSpace = `${tag} `;
if (tagColor !== undefined) {
matchedTagsHTML += `<span style="color:${tagColor}"><b>${tagWithSpace}</b></span>`;
} else if (Utils.includesTag(tag, new Set(Utils.tagBlacklist.split(" ")))) {
unmatchedTagsHTML += `<span style="color:red"><s><b>${tagWithSpace}</b></s></span>`;
} else {
unmatchedTagsHTML += tagWithSpace;
}
}
const html = matchedTagsHTML + unmatchedTagsHTML;
if (html === "") {
return tags;
}
return html;
}
/**
* @param {String} searchQuery
*/
assignTagColors(searchQuery) {
searchQuery = this.removeNotTags(searchQuery);
const {orGroups, remainingSearchTags} = Utils.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 (Utils.tagsMatchWildcardSearchTag(searchTag, [tag])) {
return this.searchTagColorCodes[searchTag];
}
}
return undefined;
}
addFavoritesOptions() {
Utils.createFavoritesOption(
"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,
"(T)"
);
}
/**
* @param {Boolean} value
*/
toggleVisibility(value) {
if (value === undefined) {
value = !this.visible;
}
Utils.setPreference("showTooltip", value);
this.visible = value;
}
/**
* @param {HTMLElement | null} thumb
*/
showOnLoadIfHoveringOverThumb(thumb) {
if (thumb !== null) {
this.show(Utils.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);
}
}
class SavedSearches {
static 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;
}
}
/* .tag-type-saved>a,
.tag-type-saved {
color: lightblue;
} */
</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 title="Export all saved searches" id="export-saved-search-button">Export</button>
<button title="Import saved searches" 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>
<script>
</script>
`;
static preferences = {
textareaWidth: "savedSearchesTextAreaWidth",
textareaHeight: "savedSearchesTextAreaHeight",
savedSearches: "savedSearches",
visibility: "savedSearchVisibility",
tutorial: "savedSearchesTutorial"
};
static localStorageKeys = {
savedSearches: "savedSearches"
};
/**
* @type {Boolean}
*/
static get disabled() {
return !Utils.onFavoritesPage() || Utils.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.insertHTML();
this.extractHTMLElements();
this.addEventListeners();
this.loadSavedSearches();
}
insertHTML() {
const showSavedSearches = Utils.getPreference(SavedSearches.preferences.visibility, false);
const savedSearchesContainer = document.getElementById("right-favorites-panel");
Utils.insertHTMLAndExtractStyle(savedSearchesContainer, "beforeend", SavedSearches.savedSearchesHTML);
document.getElementById("right-favorites-panel").style.display = showSavedSearches ? "block" : "none";
const options = Utils.createFavoritesOption(
"show-saved-searches",
"Saved Searches",
"Toggle saved searches",
showSavedSearches,
(e) => {
savedSearchesContainer.style.display = e.target.checked ? "block" : "none";
Utils.setPreference(SavedSearches.preferences.visibility, e.target.checked);
},
true
);
document.getElementById("bottom-panel-2").insertAdjacentElement("afterbegin", options);
}
extractHTMLElements() {
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");
}
addEventListeners() {
this.saveButton.onclick = () => {
this.saveSearch(this.textarea.value.trim());
};
this.textarea.addEventListener("keydown", (event) => {
switch (event.key) {
case "Enter":
if (Utils.awesompleteIsUnselected(this.textarea)) {
event.preventDefault();
this.saveButton.click();
this.textarea.blur();
setTimeout(() => {
this.textarea.focus();
}, 100);
}
break;
case "Escape":
if (Utils.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 = Utils.icons.edit;
removeButton.innerHTML = Utils.icons.delete;
moveToTopButton.innerHTML = Utils.icons.upArrow;
editButton.title = "Edit";
removeButton.title = "Delete";
moveToTopButton.title = "Move to top";
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() {
localStorage.setItem(SavedSearches.localStorageKeys.savedSearches, JSON.stringify(Utils.getSavedSearchValues()));
}
loadSavedSearches() {
const savedSearches = JSON.parse(localStorage.getItem(SavedSearches.localStorageKeys.savedSearches)) || [];
const firstUse = Utils.getPreference(SavedSearches.preferences.tutorial, true);
Utils.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 Utils.sleep(1000);
const postIds = Utils.getAllThumbs().map(thumb => thumb.id);
Utils.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(Post.allPosts.values())
.filter(post => post.matchedByMostRecentSearch)
.map(post => post.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);
}
}
class Caption {
static 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: 0.5ch;
padding-left: 7px;
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>
`;
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);
static saveTagCategoriesCooldown = new Cooldown(1000);
/**
* @type {Object.<String, Number>}
*/
static tagCategoryAssociations;
static settings = {
tagFetchDelayAfterFinishedLoading: 5,
tagFetchDelayBeforeFinishedLoading: 40
};
static flags = {
finishedLoading: false
};
/**
* @returns {String}
*/
static getCategoryHeaderHTML() {
let html = "";
for (const category of Caption.importantTagCategories) {
const capitalizedCategory = Utils.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 !Utils.onFavoritesPage() || Utils.onMobileDevice() || Utils.getPerformanceProfile() > 1;
}
/**
* @type {Boolean}
*/
get hidden() {
return this.caption.classList.contains("hide") || this.caption.classList.contains("disabled") || this.caption.classList.contains("remove");
}
/**
* @type {Number}
*/
static get tagFetchDelay() {
if (Caption.flags.finishedLoading) {
return Caption.settings.tagFetchDelayAfterFinishedLoading;
}
return Caption.settings.tagFetchDelayBeforeFinishedLoading;
}
/**
* @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.insertHTML();
this.toggleVisibility(this.getVisibilityPreference());
this.addEventListeners();
}
initializeFields() {
Caption.tagCategoryAssociations = this.loadSavedTags();
Caption.findCategoriesOnPageChangeCooldown.onDebounceEnd = () => {
this.findTagCategoriesOnPageChange();
};
Caption.saveTagCategoriesCooldown.onCooldownEnd = () => {
this.saveTagCategories();
};
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 not-highlightable";
this.captionWrapper.appendChild(this.caption);
document.head.appendChild(this.captionWrapper);
this.caption.innerHTML = Caption.template;
}
insertHTML() {
Utils.insertStyleHTML(Caption.captionHTML, "caption");
Utils.createFavoritesOption(
"show-captions",
"Details",
"Show details when hovering over thumbnail",
this.getVisibilityPreference(),
(event) => {
this.toggleVisibility(event.target.checked);
},
true,
"(D)"
);
}
/**
* @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");
}
Utils.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 = this.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" || !Utils.isHotkeyEvent(event)) {
return;
}
if (Utils.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 (Utils.onSearchPage()) {
// this.toggleVisibility();
}
}, {
passive: true
});
}
addSearchPageEventListeners() {
if (!Utils.onSearchPage()) {
return;
}
window.addEventListener("load", () => {
this.addEventListenersToThumbs.bind(this)();
}, {
once: true,
passive: true
});
}
addFavoritesPageEventListeners() {
window.addEventListener("favoritesLoaded", () => {
this.addEventListenersToThumbs.bind(this)();
Caption.flags.finishedLoading = true;
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) {
setTimeout(() => {
this.findTagCategoriesOnPageChange();
}, 100);
}
});
window.addEventListener("originalFavoritesCleared", (event) => {
const thumbs = event.detail;
const tagNames = Array.from(thumbs)
.map(thumb => Utils.getImageFromThumb(thumb).title)
.join(" ")
.split(" ")
.filter(tagName => !Utils.isNumber(tagName) && Caption.tagCategoryAssociations[tagName] === undefined);
this.findTagCategories(tagNames, Caption.tagFetchDelay, () => {
Caption.saveTagCategoriesCooldown.restart();
});
}, {
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 Utils.sleep(500);
thumbs = thumbs === undefined ? Utils.getAllThumbs() : thumbs;
for (const thumb of thumbs) {
const imageContainer = Utils.getImageFromThumb(thumb).parentElement;
if (imageContainer.onmouseenter !== null) {
continue;
}
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.preventDefault();
event.stopPropagation();
};
captionIdTag.addEventListener("contextmenu", (event) => {
event.preventDefault();
event.stopPropagation();
});
captionIdTag.onmousedown = (event) => {
event.preventDefault();
event.stopPropagation();
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 = Utils.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 = `${Utils.roundToTwoDecimalPlaces(scale)}em`;
}
/**
* @param {HTMLElement} thumb
* @returns {Boolean}
*/
thumbMetadataExists(thumb) {
if (Utils.onSearchPage()) {
return false;
}
const post = Post.allPosts.get(thumb.id);
if (post === undefined) {
return false;
}
if (post.metadata === undefined) {
return false;
}
if (post.metadata.width <= 0 || post.metadata.width <= 0) {
return false;
}
return true;
}
/**
* @param {HTMLElement} thumb
* @param {HTMLInputElement} columnInput
* @returns {Number}
*/
estimateThumbHeightFromMetadata(thumb, columnInput) {
const post = Post.allPosts.get(thumb.id);
const gridGap = 16;
const columnCount = Math.max(1, parseInt(columnInput.value));
const thumbWidthEstimate = (window.innerWidth - (columnCount * gridGap)) / columnCount;
const thumbWidthScale = post.metadata.width / thumbWidthEstimate;
return post.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();
event.preventDefault();
};
tag.addEventListener("contextmenu", (event) => {
event.preventDefault();
event.stopPropagation();
});
tag.onmousedown = (event) => {
event.preventDefault();
event.stopPropagation();
this.tagOnClick(tagName, event);
};
}
/**
* @returns {Object.<String, Number>}
*/
loadSavedTags() {
return JSON.parse(localStorage.getItem(Caption.localStorageKeys.tagCategories) || "{}");
}
saveTagCategories() {
localStorage.setItem(Caption.localStorageKeys.tagCategories, JSON.stringify(Caption.tagCategoryAssociations));
}
/**
* @param {String} tagName
* @param {MouseEvent} event
*/
tagOnClick(tagName, event) {
switch (event.button) {
case Utils.clickCodes.left:
this.tagOnClickHelper(tagName, event);
break;
case Utils.clickCodes.middle:
dispatchEvent(new CustomEvent("searchForTag", {
detail: tagName
}));
break;
case Utils.clickCodes.right:
this.tagOnClickHelper(`-${tagName}`, event);
break;
default:
break;
}
}
/**
* @param {String} value
* @param {MouseEvent} mouseEvent
*/
tagOnClickHelper(value, mouseEvent) {
if (mouseEvent.ctrlKey) {
Utils.openSearchPage(value);
return;
}
const searchBox = Utils.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 Utils.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${Utils.capitalize(tagCategory)}`;
}
/**
* @param {HTMLElement} thumb
*/
populateTags(thumb) {
const tagNames = Utils.getTagsFromThumb(thumb);
tagNames.delete(thumb.id);
const unknownThumbTags = Array.from(tagNames)
.filter(tagName => this.tagCategoryIsUnknown(thumb, tagName));
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) {
Caption.saveTagCategoriesCooldown.restart();
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(Utils.getPostPageURL(thumb.id))
.then((response) => {
return response.text();
})
.then((html) => {
const tagCategoryMap = this.getTagCategoryMapFromPostPage(html);
for (const [tagName, tagCategory] of tagCategoryMap.entries()) {
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) {
if (Caption.tagCategoryAssociations[tag] === undefined && !Utils.customTags.has(tag)) {
this.problematicTags.add(tag);
}
}
findTagCategoriesOnPageChange() {
const tagNames = this.getTagNamesWithUnknownCategories(Utils.getAllThumbs().slice(0, 200));
this.findTagCategories(tagNames, Caption.tagFetchDelay, () => {
Caption.saveTagCategoriesCooldown.restart();
});
}
/**
* @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 (Utils.isNumber(tagName) && tagName.length > 5) {
Caption.tagCategoryAssociations[tagName] = 0;
continue;
}
if (tagName.includes("'")) {
this.setAsProblematic(tagName);
}
if (this.problematicTags.has(tagName)) {
if (tagName === lastTagName) {
onAllCategoriesFound();
}
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();
}
}).catch(() => {
onAllCategoriesFound();
});
} catch (error) {
console.error(error);
}
await Utils.sleep(fetchDelay);
}
}
/**
* @param {HTMLElement[]} thumbs
* @returns {String[]}
*/
getTagNamesWithUnknownCategories(thumbs) {
const tagNamesWithUnknownCategories = new Set();
for (const thumb of thumbs) {
const tagNames = Array.from(Utils.getTagsFromThumb(thumb));
for (const tagName of tagNames) {
if (this.tagCategoryIsUnknown(thumb, tagName)) {
tagNamesWithUnknownCategories.add(tagName);
}
}
}
return Array.from(tagNamesWithUnknownCategories);
}
/**
* @param {HTMLElement} thumb
* @param {String} tagName
* @returns
*/
tagCategoryIsUnknown(thumb, tagName) {
return tagName !== thumb.id && Caption.tagCategoryAssociations[tagName] === undefined && !Utils.customTags.has(tagName);
}
}
class TagModifier {
static tagModifierHTML = `
<div id="tag-modifier-container">
<style>
#tag-modifier-ui-container {
display: none;
>* {
margin-top: 10px;
}
}
#tag-modifier-ui-textarea {
width: 80%;
}
.favorite.tag-modifier-selected {
outline: 2px dashed white !important;
>div, >a {
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<span class="option-hint"></span>
</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-buttons">
<span 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>
</span>
<span 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>
</span>
</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>
`;
/**
* @type {String}
*/
static databaseName = "AdditionalTags";
/**
* @type {String}
*/
static objectStoreName = "additionalTags";
/**
* @type {Map.<String, String>}
*/
static tagModifications = new Map();
static preferences = {
modifyTagsOutsideFavoritesPage: "modifyTagsOutsideFavoritesPage"
};
/**
* @type {Boolean}
*/
static get currentlyModifyingTags() {
return document.getElementById("tag-edit-mode") !== null;
}
/**
* @type {Boolean}
*/
static get disabled() {
if (Utils.onMobileDevice()) {
return true;
}
if (Utils.onFavoritesPage()) {
return false;
}
return Utils.getPreference(TagModifier.preferences.modifyTagsOutsideFavoritesPage, false);
}
/**
* @type {AbortController}
*/
tagEditModeAbortController;
/**
* @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}}
*/
favoritesUI;
/**
* @type {Post[]}
*/
selectedPosts;
/**
* @type {Boolean}
*/
atLeastOneFavoriteIsSelected;
constructor() {
if (TagModifier.disabled) {
return;
}
this.tagEditModeAbortController = new AbortController();
this.favoritesOption = {};
this.favoritesUI = {};
this.selectedPosts = [];
this.atLeastOneFavoriteIsSelected = false;
this.loadTagModifications();
this.insertHTML();
this.addEventListeners();
}
insertHTML() {
this.insertFavoritesPageHTML();
this.insertSearchPageHTML();
this.insertPostPageHTML();
}
insertFavoritesPageHTML() {
if (!Utils.onFavoritesPage()) {
return;
}
Utils.insertHTMLAndExtractStyle(document.getElementById("bottom-panel-4"), "beforeend", TagModifier.tagModifierHTML);
this.favoritesOption.container = document.getElementById("tag-modifier-container");
this.favoritesOption.checkbox = document.getElementById("tag-modifier-option-checkbox");
this.favoritesUI.container = document.getElementById("tag-modifier-ui-container");
this.favoritesUI.statusLabel = document.getElementById("tag-modifier-ui-status-label");
this.favoritesUI.textarea = document.getElementById("tag-modifier-ui-textarea");
this.favoritesUI.add = document.getElementById("tag-modifier-ui-add");
this.favoritesUI.remove = document.getElementById("tag-modifier-remove");
this.favoritesUI.reset = document.getElementById("tag-modifier-reset");
this.favoritesUI.selectAll = document.getElementById("tag-modifier-ui-select-all");
this.favoritesUI.unSelectAll = document.getElementById("tag-modifier-ui-un-select-all");
this.favoritesUI.import = document.getElementById("tag-modifier-import");
this.favoritesUI.export = document.getElementById("tag-modifier-export");
}
insertSearchPageHTML() {
if (!Utils.onSearchPage()) {
return;
}
1;
}
insertPostPageHTML() {
if (!Utils.onPostPage()) {
return;
}
const contentContainer = document.querySelector(".flexi");
const originalAddToFavoritesLink = Array.from(document.querySelectorAll("a")).find(a => a.textContent === "Add to favorites");
const html = `
<div style="margin-bottom: 1em;">
<h4 class="image-sublinks">
<a href="#" id="add-to-favorites">Add to favorites</a>
|
<a href="#" id="add-custom-tags">Add custom tag</a>
<select id="custom-tags-list"></select>
</h4>
</div>
`;
if (contentContainer === null || originalAddToFavoritesLink === undefined) {
return;
}
contentContainer.insertAdjacentHTML("beforebegin", html);
const addToFavorites = document.getElementById("add-to-favorites");
const addCustomTags = document.getElementById("add-custom-tags");
const customTagsList = document.getElementById("custom-tags-list");
for (const customTag of Utils.customTags.values()) {
const option = document.createElement("option");
option.value = customTag;
option.textContent = customTag;
customTagsList.appendChild(option);
}
addToFavorites.onclick = () => {
originalAddToFavoritesLink.click();
return false;
};
addCustomTags.onclick = () => {
return false;
};
}
addEventListeners() {
this.addFavoritesPageEventListeners();
this.addSearchPageEventListeners();
this.addPostPageEventListeners();
}
addFavoritesPageEventListeners() {
if (!Utils.onFavoritesPage()) {
return;
}
this.favoritesOption.checkbox.onchange = (event) => {
this.toggleTagEditMode(event.target.checked);
};
this.favoritesUI.selectAll.onclick = this.selectAll.bind(this);
this.favoritesUI.unSelectAll.onclick = this.unSelectAll.bind(this);
this.favoritesUI.add.onclick = this.addTagsToSelected.bind(this);
this.favoritesUI.remove.onclick = this.removeTagsFromSelected.bind(this);
this.favoritesUI.reset.onclick = this.resetTagModifications.bind(this);
this.favoritesUI.import.onclick = this.importTagModifications.bind(this);
this.favoritesUI.export.onclick = this.exportTagModifications.bind(this);
window.addEventListener("searchStarted", () => {
this.unSelectAll();
});
window.addEventListener("changedPage", () => {
this.highlightSelectedThumbsOnPageChange();
});
}
addSearchPageEventListeners() {
if (!Utils.onSearchPage()) {
return;
}
1;
}
addPostPageEventListeners() {
if (!Utils.onPostPage()) {
return;
}
1;
}
highlightSelectedThumbsOnPageChange() {
if (!this.atLeastOneFavoriteIsSelected) {
return;
}
const posts = Utils.getAllThumbs()
.map(thumb => Post.allPosts.get(thumb.id));
for (const post of posts) {
if (post === undefined) {
return;
}
if (this.isSelectedForModification(post)) {
this.highlightPost(post, true);
}
}
}
/**
* @param {Boolean} value
*/
toggleTagEditMode(value) {
this.toggleThumbInteraction(value);
this.toggleUI(value);
this.toggleTagEditModeEventListeners(value);
this.favoritesUI.unSelectAll.click();
}
/**
* @param {Boolean} value
*/
toggleThumbInteraction(value) {
let html = "";
if (value) {
html =
`
.favorite {
cursor: pointer;
outline: 1px solid black;
> div,
>a
{
outline: none !important;
> img {
outline: none !important;
}
pointer-events:none;
opacity: 0.6;
filter: grayscale(90%);
transition: none !important;
}
}
`;
}
Utils.insertStyleHTML(html, "tag-edit-mode");
}
/**
* @param {Boolean} value
*/
toggleUI(value) {
this.favoritesUI.container.style.display = value ? "block" : "none";
}
/**
* @param {Boolean} value
*/
toggleTagEditModeEventListeners(value) {
if (!value) {
this.tagEditModeAbortController.abort();
this.tagEditModeAbortController = new AbortController();
return;
}
document.addEventListener("click", (event) => {
if (!event.target.classList.contains(Utils.favoriteItemClassName)) {
return;
}
const post = Post.allPosts.get(event.target.id);
if (post !== undefined) {
this.toggleThumbSelection(post);
}
}, {
signal: this.tagEditModeAbortController.signal
});
}
/**
* @param {String} text
*/
showStatus(text) {
this.favoritesUI.statusLabel.style.visibility = "visible";
this.favoritesUI.statusLabel.textContent = text;
setTimeout(() => {
const statusHasNotChanged = this.favoritesUI.statusLabel.textContent === text;
if (statusHasNotChanged) {
this.favoritesUI.statusLabel.style.visibility = "hidden";
}
}, 1000);
}
unSelectAll() {
if (!this.atLeastOneFavoriteIsSelected) {
return;
}
for (const post of Post.allPosts.values()) {
this.toggleThumbSelection(post, false);
}
this.atLeastOneFavoriteIsSelected = false;
}
selectAll() {
for (const post of Post.postsMatchedBySearch.values()) {
this.toggleThumbSelection(post, true);
}
}
/**
* @param {Post} post
* @param {Boolean} value
*/
toggleThumbSelection(post, value) {
this.atLeastOneFavoriteIsSelected = true;
if (value === undefined) {
value = !this.isSelectedForModification(post);
}
post.selectedForTagModification = value ? true : undefined;
this.highlightPost(post, value);
}
/**
* @param {Post} post
* @param {Boolean} value
*/
highlightPost(post, value) {
if (post.root !== undefined) {
post.root.classList.toggle("tag-modifier-selected", value);
}
}
/**
* @param {Post} post
* @returns {Boolean}
*/
isSelectedForModification(post) {
return post.selectedForTagModification !== undefined;
}
/**
* @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.favoritesUI.textarea.value.toLowerCase();
const tagsWithoutContentTypes = this.removeContentTypeTags(tags);
const tagsToModify = Utils.removeExtraWhiteSpace(tagsWithoutContentTypes);
const statusPrefix = remove ? "Removed tag(s) from" : "Added tag(s) to";
let modifiedTagsCount = 0;
if (tagsToModify === "") {
return;
}
for (const post of Post.allPosts.values()) {
if (this.isSelectedForModification(post)) {
const additionalTags = remove ? post.removeAdditionalTags(tagsToModify) : post.addAdditionalTags(tagsToModify);
TagModifier.tagModifications.set(post.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"));
Utils.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;
}
Utils.customTags.clear();
indexedDB.deleteDatabase("AdditionalTags");
Post.allPosts.forEach(post => {
post.resetAdditionalTags();
});
dispatchEvent(new Event("modifiedTags"));
localStorage.removeItem("customTags");
}
exportTagModifications() {
const modifications = JSON.stringify(Utils.mapToObject(TagModifier.tagModifications));
navigator.clipboard.writeText(modifications);
alert("Copied tag modifications to clipboard");
}
importTagModifications() {
let modifications;
try {
const object = JSON.parse(this.favoritesUI.textarea.value);
if (!(typeof object === "object")) {
throw new TypeError(`Input parsed as ${typeof (object)}, but expected object`);
}
modifications = Utils.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);
}
}
// 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: 20,
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;
}());
class AwesompleteWrapper {
static preferences = {
savedSearchSuggestions: "savedSearchSuggestions"
};
/**
* @type {Boolean}
*/
static get disabled() {
return !Utils.onFavoritesPage();
}
/**
* @type {Boolean}
*/
showSavedSearchSuggestions;
constructor() {
if (AwesompleteWrapper.disabled) {
return;
}
this.initializeFields();
this.insertHTML();
this.addAwesompleteToInputs();
}
initializeFields() {
this.showSavedSearchSuggestions = Utils.getPreference(AwesompleteWrapper.preferences.savedSearchSuggestions, false);
}
insertHTML() {
Utils.createFavoritesOption(
"show-saved-search-suggestions",
"Saved Suggestions",
"Show saved search suggestions in autocomplete dropdown",
this.showSavedSearchSuggestions,
(event) => {
this.showSavedSearchSuggestions = event.target.checked;
Utils.setPreference(AwesompleteWrapper.preferences.savedSearchSuggestions, event.target.checked);
},
false
);
}
addAwesompleteToInputs() {
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, _) => {
// eslint-disable-next-line new-cap
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) => {
Utils.insertSuggestion(awesomplete.input, Utils.removeSavedSearchPrefix(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":
Utils.hideAwesomplete(input);
break;
default:
break;
}
});
input.oninput = () => {
this.populateAwesompleteList(input.id, this.getCurrentTagWithHyphen(input), awesomplete);
};
}
getSavedSearchesForAutocompleteList(inputId, prefix) {
if (Utils.onMobileDevice() || !this.showSavedSearchSuggestions || inputId !== "favorites-search-box") {
return [];
}
return Utils.getSavedSearchesForAutocompleteList(prefix);
}
/**
* @param {String} inputId
* @param {String} prefix
* @param {Awesomplete_} awesomplete
*/
populateAwesompleteList(inputId, prefix, awesomplete) {
if (prefix.trim() === "") {
return;
}
const savedSearchSuggestions = this.getSavedSearchesForAutocompleteList(inputId, prefix);
prefix = prefix.replace(/^-/, "");
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 = Utils.addCustomTagsToAutocompleteList(JSON.parse(suggestions), prefix);
awesomplete.list = mergedSuggestions.concat(savedSearchSuggestions);
});
}
/**
* @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];
}
/**
* @param {HTMLInputElement | HTMLTextAreaElement} input
* @returns {String}
*/
getCurrentTagWithHyphen(input) {
return this.getLastTagWithHyphen(input.value.slice(0, input.selectionStart));
}
/**
* @param {String} searchQuery
* @returns {String}
*/
getLastTagWithHyphen(searchQuery) {
const lastTag = searchQuery.match(/[^ ]*$/);
return lastTag === null ? "" : lastTag[0];
}
}
Utils.initialize();
const favoritesLoader = new FavoritesLoader();
const favoritesMenu = new FavoritesMenu();
const gallery = new Gallery();
const tooltip = new Tooltip();
const savedSearches = new SavedSearches();
const caption = new Caption();
const tagModifier = new TagModifier();
const awesompleteWrapper = new AwesompleteWrapper();