- // ==UserScript==
- // @name Rule34 Favorites Search Gallery
- // @namespace bruh3396
- // @version 1.18.2
- // @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;
- }
-
- input[type=number] {
- appearance: textfield;
- -moz-appearance: textfield;
- }
- }
-
- .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;
- }
- }
- }
-
- .blink {
- animation: blink 0.35s step-start infinite;
- }
-
- @keyframes blink {
- 0% {
- opacity: 1;
- }
-
- 50% {
- opacity: 0;
- }
-
- 100% {
- opacity: 1;
- }
- }
- </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;
- }
- }
- }`,
- darkTheme: `
- 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;
- }
- }
- `
- };
- 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();
- }
-
- static postProcess() {
- dispatchEvent(new Event("postProcess"));
- }
-
- /**
- * @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
- * @returns {any[]}
- */
- 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]
- ];
- }
- return array;
- }
-
- /**
- * @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"><span> ${optionText}</span><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: 1cqw !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() {
- window.addEventListener("postProcess", () => {
- Utils.toggleDarkTheme(Utils.usingDarkTheme());
- });
- }
-
- /**
- * @param {Boolean} value
- */
- static toggleDarkTheme(value) {
- Utils.insertStyleHTML(value ? Utils.styles.darkTheme : "", "dark-theme");
- Utils.toggleDarkStyleSheet(value);
- const currentTheme = value ? "light-green-gradient" : "dark-green-gradient";
- const targetTheme = value ? "dark-green-gradient" : "light-green-gradient";
-
- for (const element of document.querySelectorAll(`.${currentTheme}`)) {
- element.classList.remove(currentTheme);
- element.classList.add(targetTheme);
- }
- this.setCookie("theme", value ? "dark" : "light");
- }
-
- static toggleDarkStyleSheet(value) {
- const platform = Utils.onMobileDevice() ? "mobile" : "desktop";
- const darkSuffix = value ? "-dark" : "";
-
- Utils.setStyleSheet(`https://rule34.xxx//css/${platform}${darkSuffix}.css?44`);
- }
-
- /**
- * @param {String} url
- */
- static setStyleSheet(url) {
- const styleSheet = this.getMainStyleSheet();
-
- if (styleSheet !== null && styleSheet !== undefined) {
- styleSheet.href = url;
- }
- }
-
- /**
- * @returns {HTMLLinkElement}
- */
- static getMainStyleSheet() {
- return Array.from(document.querySelectorAll("link")).filter(link => link.rel === "stylesheet")[0];
- }
-
- /**
- * @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;
- let top = rect.top + window.scrollY + (rect.height / 2) - (window.innerHeight / 2) - (favoritesSearchHeight / 2);
-
- if (Utils.onMobileDevice()) {
- top = Math.max(1, top);
- }
- window.scroll({
- top,
- 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 true;
- });
- }
-
- /**
- * @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) {
- const awesomplete = Utils.getAwesompleteFromInput(input);
-
- if (awesomplete !== null) {
- awesomplete.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 desktopSuffix = Utils.onMobileDevice() ? "" : " Tag modifications and saved searches will be preserved.";
-
- const message = `Are you sure you want to reset? This will delete all cached favorites, and preferences.${desktopSuffix}`;
-
- 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) {
- return;
- }
- event.preventDefault();
- const middleClick = event.button === Utils.clickCodes.middle;
- const leftClick = event.button === Utils.clickCodes.left;
- const shiftClick = leftClick && event.shiftKey;
-
- if (leftClick && Gallery.disabled) {
- document.location = thumbURL;
- } else if (middleClick || shiftClick) {
- window.open(thumbURL);
- }
- };
- }
-
- 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();
- }
- };
- }
-
- /**
- * @returns {Boolean}
- */
- static usingIOS() {
- return (/iPhone|iPad|iPod/).test(navigator.userAgent);
- }
- }
-
- 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>
- ${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;
- const shiftClick = leftClick && event.shiftKey;
-
- if (middleClick || shiftClick || (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) {
- if (post.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}
- */
- onAllRequestsCompleted;
- /**
- * @type {Function}
- */
- onRequestCompleted;
- /**
- * @type {Set.<Number>}
- */
- pendingRequestPageNumbers;
- /**
- * @type {FavoritesPageRequest[]}
- */
- failedRequests;
- /**
- * @type {Set.<String>}
- */
- storedFavoriteIds;
- /**
- * @type {Number}
- */
- currentPageNumber;
- /**
- * @type {Boolean}
- */
- fetchedAnEmptyPage;
-
- /**
- * @type {Boolean}
- */
- get hasFailedRequests() {
- return this.failedRequests.length > 0;
- }
-
- /**
- * @type {Boolean}
- */
- get allRequestsHaveStarted() {
- return this.fetchedAnEmptyPage;
- }
-
- /**
- * @type {Boolean}
- */
- get someRequestsArePending() {
- return this.pendingRequestPageNumbers.size > 0 || this.hasFailedRequests;
- }
-
- /**
- * @type {Boolean}
- */
- get allRequestsHaveCompleted() {
- return this.allRequestsHaveStarted && !this.someRequestsArePending;
- }
-
- /**
- * @type {FavoritesPageRequest}
- */
- get oldestFailedFetchRequest() {
- return this.failedRequests.shift();
- }
-
- /**
- * @type {FavoritesPageRequest}
- */
- get newFetchRequest() {
- const request = new FavoritesPageRequest(this.currentPageNumber);
-
- this.pendingRequestPageNumbers.add(request.pageNumber);
- this.currentPageNumber += 1;
- return request;
- }
-
- /**
- * @type {FavoritesPageRequest | null}
- */
- get nextFetchRequest() {
- if (this.hasFailedRequests) {
- return this.oldestFailedFetchRequest;
- }
-
- if (!this.allRequestsHaveStarted) {
- return this.newFetchRequest;
- }
- return null;
- }
-
- /**
- * @param {Function} onAllRequestsCompleted
- * @param {Function} onRequestCompleted
- */
- constructor(onAllRequestsCompleted, onRequestCompleted) {
- this.onAllRequestsCompleted = onAllRequestsCompleted;
- this.onRequestCompleted = onRequestCompleted;
- this.storedFavoriteIds = new Set();
- this.pendingRequestPageNumbers = new Set();
- this.failedRequests = [];
- this.currentPageNumber = 0;
- this.fetchedAnEmptyPage = false;
- }
-
- async fetchAllFavorites() {
- while (!this.allRequestsHaveCompleted) {
- await this.fetchFavoritesPage(this.nextFetchRequest);
- }
- this.onAllRequestsCompleted();
- }
-
- /**
- * @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.onAllRequestsCompleted(favorites);
- return;
- }
- }
- }
-
- /**
- * @returns {Promise.<{allNewFavoritesFound: Boolean, newFavorites: Post[]}>}
- */
- fetchNewFavoritesOnReload() {
- return fetch(this.newFetchRequest.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) {
- await Utils.sleep(200);
- 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);
- this.pendingRequestPageNumbers.delete(request.pageNumber);
- const favoritesPageIsEmpty = request.fetchedFavorites.length === 0;
-
- this.fetchedAnEmptyPage = this.fetchedAnEmptyPage || favoritesPageIsEmpty;
-
- if (!favoritesPageIsEmpty) {
- this.onRequestCompleted(request);
- }
- }
-
- /**
- * @param {FavoritesPageRequest} request
- * @param {Error} error
- */
- onFavoritesPageRequestFail(request, error) {
- console.error(error);
- request.onFail();
- this.failedRequests.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() ? 4 : 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) {
- 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" && !nextPageButton.disabled;
-
- 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"));
- }
-
- if (Utils.onMobileDevice()) {
- this.paginationMenu.blur();
- }
- }
-
- /**
- * @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="#" 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, Utils.onMobileDevice() ? 10 : 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.disabled = true;
- firstPage.disabled = true;
- nextPage.disabled = true;
- finalPage.disabled = true;
- } else {
- if (firstNumberExists) {
- previousPage.disabled = true;
- firstPage.disabled = true;
- }
-
- if (lastNumberExists) {
- nextPage.disabled = true;
- finalPage.disabled = true;
- }
- }
- }
-
- /**
- * @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
- * @param {Post[]} favorites
- */
- async findFavorite(id, favorites) {
- const favoriteIds = favorites.map(favorite => favorite.id);
- const index = favoriteIds.indexOf(id);
- const favoriteNotFound = index === -1;
-
- if (favoriteNotFound) {
- return;
- }
- const pageNumber = Math.floor(index / this.favoritesPerPage) + 1;
-
- dispatchEvent(new CustomEvent("foundFavorite", {
- detail: id
- }));
-
- if (this.currentPageNumber !== pageNumber) {
- this.changePage(pageNumber, favorites);
- }
-
- await Utils.sleep(150);
- Utils.scrollToThumb(id, false, false);
- await Utils.sleep(50);
- Utils.scrollToThumb(id, false, false);
- const thumb = document.getElementById(id);
-
- if (thumb === null || thumb.classList.contains("blink")) {
- return;
- }
- thumb.classList.add("blink");
- await Utils.sleep(1500);
- thumb.classList.remove("blink");
- }
- }
-
- 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
- */
- loadFavorites(idsToDelete) {
- let loadedFavorites = {};
- let database;
-
- 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}
- */
- const favoritesDatabase = new FavoritesDatabase(null, 1);
-
- onmessage = (message) => {
- const request = message.data;
-
- switch (request.command) {
- case "create":
- favoritesDatabase.objectStoreName = request.objectStoreName;
- favoritesDatabase.version = 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 || !document.contains(this.matchCountLabel)) {
- 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 favoritesURL = Array.from(new DOMParser().parseFromString(html, "text/html").querySelectorAll("a"))
- .find(a => a.href.includes("page=favorites&s=view"));
- const favoritesCount = parseInt(favoritesURL.textContent);
-
- this.expectedTotalFavoritesCount = Math.max(favoritesCount - 2, 0);
- })
- .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.onAllRequestsCompleted = (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() {
- const prefix = Utils.onMobileDevice() ? "" : "Favorites ";
- let statusText = `Fetching ${prefix}${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));
- dispatchEvent(new Event("favoritesLoadedFromDatabase"));
- 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 === "random") {
- return Utils.shuffleArray(sortedPosts);
- }
-
- 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, this.latestSearchResults);
- }
- }
-
- 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;
- }
-
- >a>div {
- height: 100%;
- }
-
- >canvas {
- width: 100%;
- position: absolute;
- top: 0;
- left: 0;
- pointer-events: none;
- z-index: 1;
- }
- }
-
- &.hidden {
- display: none;
- }
- }
-
- .found {
- opacity: 1;
- animation: wiggle 2s;
- }
-
- @keyframes wiggle {
-
- 10%,
- 90% {
- transform: translate3d(-2px, 0, 0);
- }
-
- 20%,
- 80% {
- transform: translate3d(4px, 0, 0);
- }
-
- 30%,
- 50%,
- 70% {
- transform: translate3d(-8px, 0, 0);
- }
-
- 40%,
- 60% {
- transform: translate3d(8px, 0, 0);
- }
- }
-
- #column-resize-container {
- >div {
- align-content: center;
- }
- }
-
- #find-favorite {
- display: none;
- 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: 0.5cqw;
- }
-
- #help-links-container {
- >a:not(:last-child)::after {
- content: " |";
- }
- 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) {
- margin-bottom: 10px;
- }
-
- select {
- cursor: pointer;
- min-height: 25px;
- width: 150px;
- }
- }
-
- .number-label-container {
- display: inline-block;
- min-width: 130px;
- }
-
- #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 {
- 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;" id="search-header">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.18:</h4>
- <h5>Features:</h5>
- <ul>
- <li>Improved/fixed mobile UI</li>
- <li>Improved mobile controls</li>
- <li>Added gallery autoplay for mobile</li>
- <li>Added sort by radom (auto shuffle)</li>
- <li>Added dark theme option</li>
- <li>Minor UI fixes</li>
- <li>Minor gallery fixes</li>
- </ul>
- </div>
- </a>
- </span>
- </div>
- <div>
- <textarea name="tags" id="favorites-search-box" placeholder="Search favorites"
- 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">
- <span id="more-options-label"> More Options</span>
- <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">
- <span> Enhance Search Pages</span>
- </label>
- </div>
- <div style="display: none;">
- <label class="checkbox" title="Toggle remove buttons">
- <input type="checkbox" id="show-remove-favorite-buttons">
- <span> Remove Buttons</span>
- <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">
- <span> Add Favorite Buttons</span>
- <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">
- <span> Exclude Blacklist</span>
- </label>
- </div>
- <div>
- <label class="checkbox" title="Enable fancy image hovering (experimental)">
- <input type="checkbox" id="fancy-image-hovering-checkbox">
- <span> Fancy Hovering</span>
- </label>
- </div>
- <div style="display: none;">
- <label class="checkbox" title="Enable fancy image hovering (experimental)">
- <input type="checkbox" id="statistic-hint-checkbox">
- <span> Statistics</span>
- <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">
- <span> Hotkey Hints</span>
- <span class="option-hint"> (H)</span>
- </label>
- </div>
- <div>
- <label class="checkbox" title="Toggle dark theme">
- <input type="checkbox" id="dark-theme-checkbox">
- <span> Dark Theme</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">
- <option value="default">Default</option>
- <option value="score">Score</option>
- <option value="width">Width</option>
- <option value="height">Height</option>
- <option value="create">Date Uploaded</option>
- <option value="change">Date Changed</option>
- <option value="random">Random</option>
- </select>
- <input type="checkbox" id="sort-ascending">
- </div>
- </div>
- <div id="results-columns-container">
- <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">
- <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);
- }
- });
- }
-
- static settings = {
- mobileMenuExpandedHeight: 170,
- mobileMenuBaseHeight: 56
- };
-
- /**
- * @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.initializeFields();
- this.configureMobileUI();
- 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"),
- darkTheme: document.getElementById("dark-theme-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.updateVisibilityOfSearchClearButton();
- 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);
-
- this.checkboxes.darkTheme.checked = Utils.usingDarkTheme();
- }
-
- 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.dispatchEvent(new Event("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 = "";
-
- if (Utils.onMobileDevice()) {
- this.inputs.searchBox.focus();
- }
- this.updateVisibilityOfSearchClearButton();
- };
- 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 = () => {
- if (Utils.onMobileDevice()) {
- setTimeout(() => {
- Utils.deletePersistentData();
- }, 10);
- } else {
- 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));
- };
-
- 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", () => {
- if (!Utils.onMobileDevice()) {
- 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();
- });
- this.checkboxes.darkTheme.onchange = () => {
- Utils.toggleDarkTheme(this.checkboxes.darkTheme.checked);
- };
- }
-
- 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) {
- if (Utils.onMobileDevice()) {
- document.getElementById("left-favorites-panel-bottom-row").classList.toggle("hidden", !value);
-
- const mobileButtonRow = document.getElementById("mobile-button-row");
-
- if (mobileButtonRow !== null) {
- mobileButtonRow.style.display = value ? "" : "none";
- }
- } else {
- 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;
- }
- const minimumColumns = Utils.onMobileDevice() ? 1 : 4;
-
- count = Utils.clamp(parseInt(count), minimumColumns, 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) {
- if (header !== null) {
- header.style.display = "";
- }
- showUIContainer.insertAdjacentElement("afterbegin", showUIDiv);
- menuPanels.style.display = "flex";
- menu.removeAttribute("style");
- } else {
- menu.appendChild(showUIDiv);
-
- if (header !== null) {
- 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;
- }
- this.configureMobileStyle();
- this.setupStickyMenu();
- this.createMobileUIContainer();
- this.createResultsPerPageSelect();
- this.createColumnResizeSelect();
- this.createMobileSearchBar();
- this.createControlsGuide();
- this.createPaginationFooter();
- this.createMobileToggleSwitches();
- // this.createMobileButtonRow();
- this.createMobileSymbolRow();
- }
-
- configureMobileStyle() {
- Utils.insertStyleHTML(`
- #performance-profile-container,
- #show-hints-container,
- #whats-new-link,
- #show-ui-div,
- #search-header,
- #left-favorites-panel-top-row {
- display: none !important;
- }
-
- #favorites-pagination-container>button {
- &:active, &:focus {
- background-color: slategray;
- }
-
- &:hover {
- background-color: transparent;
- }
- }
-
- .thumb,
- .favorite {
- >div>canvas {
- display: none;
- }
- }
-
- #more-options-label {
- margin-left: 6px;
- }
-
- .checkbox {
- margin-bottom: 8px;
-
- input[type="checkbox"] {
- margin-right: 10px;
- }
- }
-
- #mobile-container {
- position: fixed !important;
- z-index: 30;
- width: 100vw;
- top: 0px;
- left: 0px;
- }
-
- #favorites-search-gallery-menu-panels {
- display: block !important;
- }
-
- #right-favorites-panel {
- margin-left: 0px !important;
- }
-
- #left-favorites-panel-bottom-row {
- margin: 4px 0px 0px 0px !important;
- }
-
- #additional-favorite-options-container {
- margin-right: 5px;
- }
-
- #favorites-search-gallery-content {
- grid-gap: 1.2cqw;
- }
-
- #favorites-search-gallery-menu {
- padding: 7px 5px 5px 5px;
- top: 0;
- left: 0;
- width: 100vw;
-
-
- &.fixed {
- position: fixed;
- margin-top: 0;
- }
- }
-
- #favorites-load-status-label {
- display: inline;
- }
-
- textarea {
- border-radius: 0px;
- height: 50px;
- padding: 8px 0px 8px 10px !important;
- }
-
- body {
- width: 100% !important;
- }
-
- #favorites-pagination-container>button {
- text-align: center;
- font-size: 16px;
- height: 30px;
- min-width: 30px;
- }
-
- #goto-page-input {
- top: -1px;
- position: relative;
- height: 25px;
- width: 1em !important;
- text-align: center;
- font-size: 16px;
- }
-
- #goto-page-button {
- display: none;
- height: 36px;
- position: absolute;
- margin-left: 5px;
- }
-
- #additional-favorite-options {
- .number {
- display: none;
- }
- }
-
- #results-per-page-container {
- margin-bottom: 10px;
- }
-
- #bottom-panel-3,
- #bottom-panel-4 {
- flex: none !important;
- }
-
- #bottom-panel-2 {
- padding-top: 8px;
- }
-
- #rating-container {
- position: relative;
- left: -5px;
- top: -2px;
- display: none;
- }
-
- #favorites-pagination-container>button {
- &[disabled] {
- opacity: 0.25;
- pointer-events: none;
- }
- }
-
- html {
- -webkit-tap-highlight-color: transparent;
- -webkit-text-size-adjust: 100%;
- }
-
- #additional-favorite-options {
- select {
- width: 120px;
- }
- }
-
- .add-or-remove-button {
- filter: none;
- width: 60%;
- }
-
- #left-favorites-panel-bottom-row {
- height: ${FavoritesMenu.settings.mobileMenuExpandedHeight}px;
- overflow: hidden;
- transition: height 0.2s ease;
- -webkit-transition: height 0.2s ease;
- -moz-transition: height 0.2s ease;
- -ms-transition: height 0.2s ease;
- -o-transition: height 0.2s ease;
- transition: height 0.2s ease;
-
- &.hidden {
- height: 0px;
- }
- }
-
- #favorites-search-gallery-content.sticky {
- transition: margin 0.2s ease;
- }
-
- #autoplay-settings-menu {
- >div {
- font-size: 14px !important;
- }
- }
-
- #results-columns-container {
- margin-top: -6px;
- }
- `, "mobile");
- document.getElementById("sorting-method").parentElement.style.marginTop = "-5px";
- document.getElementById("more-options-label").textContent = " Options";
- document.getElementById("options-checkbox").parentElement.style.display = "none";
- const experimentalLayoutEnabled = Utils.getCookie("experiment-mobile-layout", "true");
-
- if (experimentalLayoutEnabled === "true") {
- Utils.insertStyleHTML(`
- input[type="checkbox"] {
- height: 18px;
- }
- `, "experimental-mobile");
- } else {
- Utils.insertStyleHTML(`
- input[type="checkbox"] {
- width: 25px;
- height: 25px;
- }
- `, "non-experimental-mobile");
- }
-
- if (Utils.usingIOS) {
- const viewportMeta = Array.from(document.getElementsByName("viewport"))[0];
-
- if (viewportMeta !== undefined) {
- viewportMeta.setAttribute("content", `${viewportMeta.getAttribute("content")}, maximum-scale:1.0, user-scalable=0`);
- }
- }
- }
-
- createMobileUIContainer() {
- const mobileUIContainer = document.createElement("div");
- const header = document.getElementById("header");
- const menu = document.getElementById("favorites-search-gallery-menu");
-
- mobileUIContainer.id = "mobile-header";
- Utils.favoritesSearchGalleryContainer.insertAdjacentElement("afterbegin", mobileUIContainer);
-
- if (header !== null) {
- mobileUIContainer.appendChild(header);
- }
- mobileUIContainer.appendChild(menu);
- }
-
- setupStickyMenu() {
- const header = document.getElementById("header");
- const headerHeight = header === null ? 0 : header.getBoundingClientRect().height;
-
- window.addEventListener("scroll", async() => {
- if (window.scrollY > headerHeight && document.getElementById("sticky-header-fsg-style") === null) {
- Utils.insertStyleHTML(
- `
- #favorites-search-gallery-menu {
- position: fixed;
- margin-top: 0;
- }
- `,
- "sticky-header"
- );
- this.updateOptionContentMargin();
- await Utils.sleep(1);
- document.getElementById("favorites-search-gallery-content").classList.add("sticky");
-
- } else if (window.scrollY <= headerHeight && document.getElementById("sticky-header-fsg-style") !== null) {
- document.getElementById("sticky-header-fsg-style").remove();
- document.getElementById("favorites-search-gallery-content").classList.remove("sticky");
- this.removeOptionContentMargin();
- }
- }, {
- passive: true
- });
- }
-
- createResultsPerPageSelect() {
- const resultsPerPageSelectHTML = `
- <select id="results-per-page-select">
- <option value="50">50</option>
- <option value="100">100</option>
- <option value="200">200</option>
- <option value="500">500</option>
- <option value="1000">1000</option>
- <option value="2000">2000</option>
- <option value="5000">5000</option>
- </select>
- `;
-
- document.getElementById("results-per-page-container").querySelector(".number")
- .insertAdjacentHTML("afterend", resultsPerPageSelectHTML);
- const resultsPerPageSelect = document.getElementById("results-per-page-select");
-
- resultsPerPageSelect.value = Utils.getPreference(this.preferences.resultsPerPage, Utils.defaults.resultsPerPage);
- resultsPerPageSelect.onchange = () => {
- this.changeResultsPerPage(parseInt(resultsPerPageSelect.value));
- };
- }
-
- createColumnResizeSelect() {
- const columnResizeSelect = document.createElement("select");
- const columnResizeNumberInput = document.getElementById("column-resize-container").querySelector(".number");
-
- for (let i = 1; i <= 10; i += 1) {
- const option = document.createElement("option");
-
- option.value = i;
- option.textContent = i;
- columnResizeSelect.appendChild(option);
- }
- columnResizeSelect.value = Utils.getPreference(this.preferences.columnCount, Utils.defaults.columnCount);
- columnResizeSelect.onchange = () => {
- this.changeColumnCount(parseInt(columnResizeSelect.value));
- };
- columnResizeNumberInput.insertAdjacentElement("afterend", columnResizeSelect);
- }
-
- createMobileSearchBar() {
- document.getElementById("clear-button").remove();
- document.getElementById("search-button").remove();
- document.getElementById("options-checkbox").remove();
- document.getElementById("reset-button").remove();
-
- Utils.insertStyleHTML(`
- #mobile-toolbar-row {
- display: flex;
- align-items: center;
- background: none;
-
- svg {
- fill: black;
- -webkit-transition: none;
- transition: none;
- transform: scale(0.85);
- }
-
- input[type="checkbox"]:checked + label {
- svg {
- fill: #0075FF;
- }
- color: #0075FF;
- }
-
- .dark-green-gradient {
- svg {
- fill: white;
- }
- }
- }
- .search-bar-container {
- align-content: center;
- width: 100%;
- height: 40px;
- border-radius: 50px;
- padding-left: 10px;
- padding-right: 10px;
- flex: 1;
- background: white;
-
- &.dark-green-gradient {
- background: #303030;
- }
- }
-
- .search-bar-items {
- display: flex;
- align-items: center;
- height: 100%;
- width: 100%;
-
- > div {
- flex: 0;
- min-width: 40px;
- width: 100%;
- height: 100%;
- display: block;
- align-content: center;
- }
- }
-
- .search-icon-container {
- flex: 0;
- min-width: 40px;
- }
-
- .search-bar-input-container {
- flex: 1 !important;
- display: flex;
- width: 100%;
- height: 100%;
- }
-
- .search-bar-input {
- flex: 1;
- border: none;
- box-sizing: content-box;
- height: 100%;
- padding: 0;
- margin: 0;
- outline: none !important;
- border: none !important;
- font-size: 14px !important;
- width: 100%;
-
- &:focus, &:focus-visible {
- background: none !important;
- border: none !important;
- outline: none !important;
- }
- }
-
- .search-clear-container {
- visibility: hidden;
-
- svg {
- transition: none !important;
- transform: scale(0.6) !important;
- }
- }
-
- .circle-icon-container {
- padding: 0;
- margin: 0;
- align-content: center;
- border-radius: 50%;
-
- &:active {
- background-color: #0075FF;
- }
- }
-
- #options-checkbox {
- display: none;
- }
-
- .mobile-toolbar-checkbox-label {
- width: 100%;
- height: 100%;
- display: block;
- }
-
- #reset-button {
- transition: none !important;
- height: 100%;
-
- >svg {
- transition: none !important;
- transform: scale(0.65);
- }
-
- &:active {
- svg {
- fill: #0075FF;
- }
- }
- }
-
- #help-button {
- height: 100%;
-
- >svg {
- transform: scale(0.75);
- }
- }
-
- .
- `, "mobile-toolbar");
-
- const searchBar = document.getElementById("favorites-search-box");
- const mobileSearchBarHTML = `
- <div id="mobile-toolbar-row" class="light-green-gradient">
- <div class="search-bar-container light-green-gradient">
- <div class="search-bar-items">
- <div>
- <div class="circle-icon-container">
- <svg class="search-icon" id="search-button" xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M784-120 532-372q-30 24-69 38t-83 14q-109 0-184.5-75.5T120-580q0-109 75.5-184.5T380-840q109 0 184.5 75.5T640-580q0 44-14 83t-38 69l252 252-56 56ZM380-400q75 0 127.5-52.5T560-580q0-75-52.5-127.5T380-760q-75 0-127.5 52.5T200-580q0 75 52.5 127.5T380-400Z"/>
- </svg>
- </div>
- </div>
- <div class="search-bar-input-container">
- <input type="text" id="favorites-search-box" class="search-bar-input" needs-autocomplete placeholder="Search favorites">
- </div>
- <div class="toolbar-button search-clear-container">
- <div class="circle-icon-container">
- <svg id="clear-button" fill="currentColor" xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="m256-200-56-56 224-224-224-224 56-56 224 224 224-224 56 56-224 224 224 224-56 56-224-224-224 224Z"/>
- </svg>
- </div>
- </div>
- <div>
- <input type="checkbox" id="options-checkbox">
- <label for="options-checkbox" class="mobile-toolbar-checkbox-label"><svg id="options-menu-icon" fill="currentColor" xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="#5f6368"><path d="M120-240v-80h720v80H120Zm0-200v-80h720v80H120Zm0-200v-80h720v80H120Z"/></svg></label>
- </div>
- <div>
- <div id="reset-button">
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="currentColor"><path d="M480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-84 31.5-156.5T197-763l56 56q-44 44-68.5 102T160-480q0 134 93 227t227 93q134 0 227-93t93-227q0-67-24.5-125T707-707l56-56q54 54 85.5 126.5T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm-40-360v-440h80v440h-80Z"/></svg>
- </div>
- </div>
- <div style="display: none;">
- <div id="">
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="currentColor"><path d="M424-320q0-81 14.5-116.5T500-514q41-36 62.5-62.5T584-637q0-41-27.5-68T480-732q-51 0-77.5 31T365-638l-103-44q21-64 77-111t141-47q105 0 161.5 58.5T698-641q0 50-21.5 85.5T609-475q-49 47-59.5 71.5T539-320H424Zm56 240q-33 0-56.5-23.5T400-160q0-33 23.5-56.5T480-240q33 0 56.5 23.5T560-160q0 33-23.5 56.5T480-80Z"/></svg>
- </div>
- </div>
- </div>
- </div>
- </div>
- `;
-
- searchBar.insertAdjacentHTML("afterend", mobileSearchBarHTML);
- searchBar.remove();
- document.getElementById("favorites-search-box").addEventListener("input", () => {
- this.updateVisibilityOfSearchClearButton();
- });
- document.getElementById("options-checkbox").addEventListener("change", (event) => {
- const menuIsSticky = document.getElementById("favorites-search-gallery-content").classList.contains("sticky");
- const margin = event.target.checked ? FavoritesMenu.settings.mobileMenuBaseHeight + FavoritesMenu.settings.mobileMenuExpandedHeight : FavoritesMenu.settings.mobileMenuBaseHeight;
-
- if (menuIsSticky) {
- Utils.sleep(1);
- this.updateOptionContentMargin(margin);
- }
- });
- }
-
- createPaginationFooter() {
- Utils.insertStyleHTML(`
- #mobile-footer {
- position: fixed;
- width: 100%;
- bottom: 0;
- left: 0;
- padding: 4px 0px;
- > div {
- text-align: center;
- }
-
- &.light-green-gradient {
- background: linear-gradient(to top, #aae5a4, #89e180);
- }
- &.dark-green-gradient {
- background: linear-gradient(to top, #5e715e, #293129);
-
- }
- }
-
- #mobile-footer-top {
- margin-bottom: 4px;
- }
-
- #favorites-search-gallery-content {
- margin-bottom: 20px;
- }
-
- #favorites-load-status {
- font-size: 12px !important;
- >span {
- margin-right: 10px;
- }
-
- >span:nth-child(odd) {
- font-weight: bold;
- }
- }
-
- #favorites-load-status-label {
- padding-left: 0 !important;
- }
-
- #pagination-number:active {
- opacity: 0.5;
- filter: none !important;
- }
- `, "mobile-footer");
- const footerHTML = `
- <div id="mobile-footer" class="light-green-gradient">
- <div id="mobile-footer-header"></div>
- <div id="mobile-footer-top"></div>
- <div id="mobile-footer-bottom"></div>
- </div>
- `;
- const loadStatus = document.getElementById("favorites-load-status");
-
- for (const label of Array.from(loadStatus.querySelectorAll("label"))) {
- const span = document.createElement("span");
-
- span.id = label.id;
- span.className = label.className;
- span.innerHTML = label.innerHTML;
- label.remove();
- loadStatus.appendChild(span);
- }
- Utils.insertFavoritesSearchGalleryHTML("beforeend", footerHTML);
- const footerHeader = document.getElementById("mobile-footer-header");
- const footerTop = document.getElementById("mobile-footer-top");
- const footerBottom = document.getElementById("mobile-footer-bottom");
-
- footerHeader.appendChild(document.getElementById("help-links-container"));
- footerTop.appendChild(document.getElementById("favorites-load-status"));
- footerBottom.appendChild(document.getElementById("favorites-pagination-placeholder"));
- document.getElementById("whats-new-link").remove();
- }
-
- createControlsGuide() {
- Utils.insertStyleHTML(`
- #controls-guide {
- display: none;
- z-index: 99999;
- --tap-control: blue;
- --swipe-down: red;
- --swipe-up: green;
- top: 0;
- left: 0;
- background: lightblue;
- width: 100%;
- height: 100%;
- padding: 0;
- margin: 0;
- flex-direction: column;
- position: fixed;
-
- &.active {
- display: flex;
- }
- }
-
- #controls-guide-image-container {
- background: black;
- width: 100%;
- height: 100%;
- }
-
- #controls-guide-sample-image {
- background: lightblue;
- position: relative;
- top: 50%;
- left: 0;
- width: 100%;
- transform: translateY(-50%);
- }
-
- #controls-guide-top {
- position: relative;
- flex: 3;
- }
-
- #controls-guide-bottom {
- flex: 1;
- min-height: 25%;
- padding: 10px;
- font-size: 20px;
- align-content: center;
- }
-
- #controls-guide-tap-container {
- width: 100%;
- height: 100%;
- position: absolute;
- }
- .controls-guide-tap {
- color: white;
- font-size: 50px;
- position: absolute;
- top: 50%;
- height: 65%;
- width: 15%;
- background: var(--tap-control);
- z-index: 9999;
- transform: translateY(-50%);
- writing-mode: vertical-lr;
- text-align: center;
- opacity: 0.8;
- }
-
- #controls-guide-tap-right {
- right: 0;
- }
- #controls-guide-tap-left {
- left: 0;
- }
- #controls-guide-swipe-container {
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
-
- svg {
- position: absolute;
- left: 50%;
- transform: translateX(-50%);
- width: 25%;
- }
- }
-
- #controls-guide-swipe-down {
- top: 0;
- color: var(--swipe-down);
- fill: var(--swipe-down);
- }
-
- #controls-guide-swipe-up {
- bottom: 0;
- color: var(--swipe-up);
- fill: var(--swipe-up);
- }
- `, "controls-guide");
- Utils.insertFavoritesSearchGalleryHTML("beforeend", `
- <div id="controls-guide">
- <div id="controls-guide-top">
- <div id="controls-guide-tap-container">
- <div id="controls-guide-tap-left" class="controls-guide-tap">
- Previous
- </div>
- <div id="controls-guide-tap-right" class="controls-guide-tap">
- Next
- </div>
- </div>
- <div id="controls-guide-image-container">
- <img id="controls-guide-sample-image" src="https://rule34.xxx/images/header2.png">
- </div>
- <div id="controls-guide-swipe-container">
- <svg id="controls-guide-swipe-down" xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M180-360 40-500l42-42 70 70q-6-27-9-54t-3-54q0-82 27-159t78-141l43 43q-43 56-65.5 121.5T200-580q0 26 3 51.5t10 50.5l65-64 42 42-140 140Zm478 233q-23 8-46.5 7.5T566-131L304-253l18-40q10-20 28-32.5t40-14.5l68-5-112-307q-6-16 1-30.5t23-20.5q16-6 30.5 1t20.5 23l148 407-100 7 131 61q7 3 15 3.5t15-1.5l157-57q31-11 45-41.5t3-61.5l-55-150q-6-16 1-30.5t23-20.5q16-6 30.5 1t20.5 23l55 150q23 63-4.5 122.5T815-184l-157 57Zm-90-265-54-151q-6-16 1-30.5t23-20.5q16-6 30.5 1t20.5 23l55 150-76 28Zm113-41-41-113q-6-16 1-30.5t23-20.5q16-6 30.5 1t20.5 23l41 112-75 28Zm8 78Z"/></svg>
- <svg id="controls-guide-swipe-up" xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M245-400q-51-64-78-141t-27-159q0-27 3-54t9-54l-70 70-42-42 140-140 140 140-42 42-65-64q-7 25-10 50.5t-3 51.5q0 70 22.5 135.5T288-443l-43 43Zm413 273q-23 8-46.5 7.5T566-131L304-253l18-40q10-20 28-32.5t40-14.5l68-5-112-307q-6-16 1-30.5t23-20.5q16-6 30.5 1t20.5 23l148 407-100 7 131 61q7 3 15 3.5t15-1.5l157-57q31-11 45-41.5t3-61.5l-55-150q-6-16 1-30.5t23-20.5q16-6 30.5 1t20.5 23l55 150q23 63-4.5 122.5T815-184l-157 57Zm-90-265-54-151q-6-16 1-30.5t23-20.5q16-6 30.5 1t20.5 23l55 150-76 28Zm113-41-41-113q-6-16 1-30.5t23-20.5q16-6 30.5 1t20.5 23l41 112-75 28Zm8 78Z"/></svg>
- </div>
- </div>
- <div id="controls-guide-bottom">
- <ul style="text-align: center; list-style: none;">
- <li style="color: var(--tap-control);">Tap edges to traverse gallery</li>
- <li style="color: var(--swipe-down);">Swipe down to exit gallery</li>
- <li style="color: var(--swipe-up);">Swipe up to open autoplay menu</li>
- </ul>
- </div>
- </div>
- `);
- const controlGuide = document.getElementById("controls-guide");
- const anchor = document.createElement("a");
-
- anchor.textContent = "Controls";
- anchor.href = "#";
- anchor.onmousedown = (event) => {
- event.preventDefault();
- event.stopPropagation();
- controlGuide.classList.toggle("active", true);
- };
- controlGuide.ontouchstart = (event) => {
- event.preventDefault();
- event.stopPropagation();
- controlGuide.classList.toggle("active", false);
- };
-
- document.getElementById("help-links-container").insertAdjacentElement("afterbegin", anchor);
- controlGuide.onmousedown = () => {
- controlGuide.classList.toggle("active", false);
- };
- }
-
- createMobileToggleSwitches() {
- window.addEventListener("postProcess", () => {
- setTimeout(() => {
- this.createMobileToggleSwitchesHelper();
- }, 10);
- }, {
- once: true
- });
- }
-
- createMobileToggleSwitchesHelper() {
- Utils.insertStyleHTML(`
- .toggle-switch {
- position: relative;
- display: inline-block;
- width: 60px;
- height: 34px;
- transform: scale(.75);
- align-content: center;
- }
-
- .toggle-switch input {
- opacity: 0;
- width: 0;
- height: 0;
- }
-
- .slider {
- position: absolute;
- cursor: pointer;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background-color: #ccc;
- -webkit-transition: .4s;
- transition: .4s;
- }
-
- .slider:before {
- position: absolute;
- content: "";
- height: 26px;
- width: 26px;
- left: 4px;
- bottom: 4px;
- background-color: white;
- -webkit-transition: .4s;
- transition: .4s;
- }
-
- input:checked + .slider {
- background-color: #0075FF;
- }
-
- input:focus + .slider {
- box-shadow: 0 0 1px #0075FF;
- }
-
- input:checked + .slider:before {
- -webkit-transform: translateX(26px);
- -ms-transform: translateX(26px);
- transform: translateX(26px);
- }
-
- .slider.round {
- border-radius: 34px;
- }
-
- .slider.round:before {
- border-radius: 50%;
- }
-
- .toggle-switch-label {
- margin-left: 60px;
- margin-top: 20px;
- font-size: 16px;
- }
-
- #sort-ascending {
- width: 0 !important;
- height: 0 !important;
- position: static !important;
- }
-
- `, "mobile-toggle-switch");
- const checkboxes = Array.from(document.querySelectorAll(".checkbox"))
- .filter(checkbox => checkbox.getBoundingClientRect().width > 0);
-
- for (const hint of Array.from(document.querySelectorAll(".option-hint"))) {
- hint.remove();
- }
-
- for (const checkbox of checkboxes) {
- const label = checkbox.querySelector("span");
- const input = checkbox.querySelector("input");
- const slider = document.createElement("span");
-
- if (input === null) {
- continue;
- }
- slider.className = "slider round";
- checkbox.className = "toggle-switch";
- input.insertAdjacentElement("afterend", slider);
-
- if (label !== null) {
- label.className = "toggle-switch-label";
- }
- }
- const sortAscendingCheckbox = document.getElementById("sort-ascending");
-
- if (sortAscendingCheckbox !== null) {
- const container = document.createElement("span");
- const toggleSwitch = document.createElement("label");
- const slider = document.createElement("span");
-
- toggleSwitch.className = "toggle-switch";
- toggleSwitch.style.transform = "scale(0.6)";
- toggleSwitch.style.marginLeft = "-12px";
- slider.className = "slider round";
- sortAscendingCheckbox.insertAdjacentElement("beforebegin", container);
- container.appendChild(toggleSwitch);
- toggleSwitch.appendChild(sortAscendingCheckbox);
- toggleSwitch.appendChild(slider);
- sortAscendingCheckbox.insertAdjacentElement("afterend", slider);
- }
- }
-
- createMobileButtonRow() {
- const buttonHeight = 30;
-
- Utils.insertStyleHTML(`
- #mobile-button-row {
- padding: 0;
- position: absolute;
- width: 98%;
- display: flex;
- gap: 10px;
- padding: 0px 20px;
-
- >button, >div {
- font-size: 20px;
- flex: 1;
- height: ${buttonHeight}px;
- border-radius: 30px;
- }
- }
-
- #left-favorites-panel-bottom-row>div:not(:first-child) {
- margin-top:${buttonHeight}px
- }
- `, "mobile-button");
-
- const html = `
- <div id="mobile-button-row">
- <button>Reset</button>
- <button>Help</button>
- <button>Shuffle</button>
- </div>
- `;
-
- document.getElementById("left-favorites-panel-bottom-row").insertAdjacentHTML("afterbegin", html);
- }
-
- createMobileSymbolRow() {
- Utils.insertStyleHTML(`
- #mobile-symbol-container {
- display: flex;
- gap: 10px;
- text-align: center;
- height: 0;
- overflow: hidden;
- width: 100%;
- transition: height .2s ease;
-
- >button {
- font-size: 20px;
- padding: 0;
- margin: 0;
- font-weight: bold;
- text-align: center;
- flex: 1;
- height: 100% !important;
- }
-
- &.active {
- height: 30px;
- }
- }
- `);
- document.getElementById("left-favorites-panel")
- .insertAdjacentHTML("afterbegin", `
- <div id="mobile-symbol-container">
- <button>-</button>
- <button>*</button>
- <button>_</button>
- <button>(</button>
- <button>)</button>
- <button>~</button>
- </div>
- `);
- const mobileSymbolContainer = document.getElementById("mobile-symbol-container");
- /**
- * @type {HTMLInputElement}
- */
-
- const searchBar = document.getElementById("favorites-search-box");
-
- for (const button of Array.from(document.getElementById("mobile-symbol-container").querySelectorAll("button"))) {
- button.addEventListener("blur", async(event) => {
- await Utils.sleep(0);
-
- if (document.activeElement.id !== "favorites-search-box" && !mobileSymbolContainer.contains(document.activeElement)) {
- mobileSymbolContainer.classList.toggle("active", false);
- }
- });
-
- button.addEventListener("click", () => {
- const value = searchBar.value;
- const selectionStart = searchBar.selectionStart;
-
- searchBar.value = value.slice(0, selectionStart) + button.textContent + value.slice(selectionStart);
- this.updateVisibilityOfSearchClearButton();
- searchBar.selectionStart = selectionStart + 1;
- searchBar.selectionEnd = selectionStart + 1;
- searchBar.focus();
- }, {
- passive: true
- });
- }
-
- window.addEventListener("postProcess", () => {
-
- searchBar.addEventListener("focus", () => {
- document.getElementById("mobile-symbol-container").classList.toggle("active", true);
- }, {
- passive: true
- });
-
- searchBar.addEventListener("blur", async(event) => {
- await Utils.sleep(10);
-
- if (document.activeElement.id !== "favorites-search-box" && !mobileSymbolContainer.contains(document.activeElement)) {
- mobileSymbolContainer.classList.toggle("active", false);
- }
- });
- }, {
- once: true
- });
- }
-
- clickedOnSearchItem(event) {
-
- }
-
- updateVisibilityOfSearchClearButton() {
- if (!Utils.onMobileDevice()) {
- return;
- }
- const clearButtonContainer = document.querySelector(".search-clear-container");
-
- if (clearButtonContainer === null) {
- return;
- }
-
- const clearButtonIsHidden = getComputedStyle(clearButtonContainer).visibility === "hidden";
- const searchBarIsEmpty = this.inputs.searchBox.value === "";
- const styleId = "search-clear-button-visibility";
-
- if (searchBarIsEmpty && !clearButtonIsHidden) {
- Utils.insertStyleHTML(".search-clear-container {visibility: hidden}", styleId);
- } else if (!searchBarIsEmpty && clearButtonIsHidden) {
- Utils.insertStyleHTML(".search-clear-container {visibility: visible}", styleId);
- }
- }
-
- /**
- * @param {Number} margin
- */
- updateOptionContentMargin(margin) {
- margin = margin === undefined ? document.getElementById("favorites-search-gallery-menu").getBoundingClientRect().height + 11 : margin;
- Utils.insertStyleHTML(`
- #favorites-search-gallery-content {
- margin-top: ${margin}px;
- }`, "options-content-margin");
- }
-
- removeOptionContentMargin() {
- const optionsContentMargin = document.getElementById("options-content-margin-fsg-style");
-
- if (optionsContentMargin !== null) {
- optionsContentMargin.remove();
- }
- }
-
- 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;
- }
- }
- }
-
- select {
- /* height: 25px; */
- font-size: larger;
- width: 10ch;
- }
- }
-
- #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: Utils.onMobileDevice() ? 1500 : 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 false;
- // 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.configureMobileUi();
- this.extractUiElements();
- 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, Utils.onMobileDevice());
- 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);
- }
-
- 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");
- }
-
- extractUiElements() {
- 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");
- }
-
- configureMobileUi() {
- this.createViewDurationSelects();
- }
-
- createViewDurationSelects() {
- const imageViewDurationSelect = this.createDurationSelect(1, 60);
- const videoViewDurationSelect = this.createDurationSelect(0, 60);
- const imageViewDurationInput = document.getElementById("autoplay-image-duration-input").parentElement;
- const videoViewDurationInput = document.getElementById("autoplay-minimum-animated-duration-input").parentElement;
-
- imageViewDurationSelect.value = Autoplay.settings.imageViewDurationInSeconds;
- videoViewDurationSelect.value = Autoplay.settings.minimumVideoDurationInSeconds;
- imageViewDurationInput.insertAdjacentElement("afterend", imageViewDurationSelect);
- videoViewDurationInput.insertAdjacentElement("afterend", videoViewDurationSelect);
- imageViewDurationInput.remove();
- videoViewDurationInput.remove();
- imageViewDurationSelect.id = "autoplay-image-duration-input";
- videoViewDurationSelect.id = "autoplay-minimum-animated-duration-input";
- }
-
- /**
- * @param {Number} minimum
- * @param {Number} maximum
- * @returns {HTMLSelectElement}
- */
- createDurationSelect(minimum, maximum) {
- const select = document.createElement("select");
-
- for (let i = minimum; i <= maximum; i += 1) {
- const option = document.createElement("option");
-
- switch (true) {
- case i <= 5:
- break;
-
- case i <= 20:
- i += 4;
- break;
-
- case i <= 30:
- i += 9;
- break;
-
- default:
- i += 29;
- break;
- }
- option.value = i;
- option.innerText = i;
- select.append(option);
- }
- select.ontouchstart = () => {
- select.dispatchEvent(new Event("mousedown"));
- };
- return select;
- }
-
- 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.addDesktopMenuEventListeners();
- this.addMobileMenuEventListeners();
- }
-
- addDesktopMenuEventListeners() {
- if (Utils.onMobileDevice()) {
- return;
- }
- this.ui.settingsButton.onclick = () => {
- this.toggleSettingMenu();
- };
- this.ui.playButton.onclick = () => {
- this.pause();
- };
- this.ui.changeDirectionButton.onclick = () => {
- this.toggleDirection();
- };
- this.ui.menu.onmouseenter = () => {
- this.toggleMenuPersistence(true);
- };
- this.ui.menu.onmouseleave = () => {
- this.toggleMenuPersistence(false);
- };
- }
-
- addMobileMenuEventListeners() {
- if (!Utils.onMobileDevice()) {
- return;
- }
- this.ui.settingsButton.ontouchstart = () => {
- this.toggleSettingMenu();
- const settingsMenuIsVisible = this.ui.settingsMenu.container.classList.contains("visible");
-
- this.toggleMenuPersistence(settingsMenuIsVisible);
- this.menuVisibilityTimer.restart();
- };
- this.ui.playButton.ontouchstart = () => {
- this.pause();
- this.menuVisibilityTimer.restart();
- };
- this.ui.changeDirectionButton.ontouchstart = () => {
- this.toggleDirection();
- this.menuVisibilityTimer.restart();
- };
- }
-
- 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} forward
- */
- toggleDirection(forward) {
- const directionHasNotChanged = forward === Autoplay.settings.moveForward;
-
- if (directionHasNotChanged) {
- return;
- }
- 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);
- }
-
- /**
- * @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;
-
- case " ":
- if (this.currentThumb !== null && !Utils.isVideo(this.currentThumb)) {
- 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 VideoClip {
- /**
- * @type {Number}
- */
- start;
- /**
- * @type {Number}
- */
- end;
-
- /**
- * @param {{start: Number, end: Number}} videoClip
- */
- constructor(videoClip) {
- this.start = videoClip.start;
- this.end = videoClip.end;
- }
- }
-
- 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;
- }
- }
-
- #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;
- }
-
- #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;
- }
-
- #original-content-background-link-mask {
- position: fixed;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- background: red;
- z-index: 10001;
- pointer-events: none;
- cursor: default;
- display: none;
- opacity: 0;
- -webkit-user-drag: none;
- -khtml-user-drag: none;
- -moz-user-drag: none;
- -o-user-drag: none;
-
- &.active {
- /* opacity: 0.2; */
- pointer-events: all;
- }
- }
-
- #original-gif-container {
- z-index: 9995;
- }
- </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 canvasResolutions = {
- search: "3840x2160",
- favorites: Utils.onMobileDevice() ? "1920x1080" : "7680x4320",
- low: Utils.onMobileDevice() ? "640x360" : "1280:720"
- };
- 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 commonVideoAttributes = "width=\"100%\" height=\"100%\" autoplay muted loop controlsList=\"nofullscreen\" webkit-playsinline playsinline";
- static settings = {
- maxImagesToRenderInBackground: 50,
- maxImagesToRenderAround: Utils.onMobileDevice() ? 3 : 50,
- megabyteLimit: Utils.onMobileDevice() ? 0 : 400,
- minImagesToRender: Utils.onMobileDevice() ? 3 : 8,
- imageFetchDelay: 250,
- throttledImageFetchDelay: 400,
- imageFetchDelayWhenExtensionKnown: Utils.onMobileDevice() ? 50 : 25,
- upscaledThumbResolutionFraction: 4,
- upscaledAnimatedThumbResolutionFraction: 6,
- animatedThumbsToUpscaleRange: 20,
- animatedThumbsToUpscaleDiscrete: 20,
- traversalCooldownTime: 300,
- renderOnPageChangeCooldownTime: 2000,
- addFavoriteCooldownTime: 250,
- cursorVisibilityCooldownTime: 500,
- imageExtensionAssignmentCooldownTime: 1000,
- additionalVideoPlayerCount: Utils.onMobileDevice() ? 2 : 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 {HTMLDivElement}
- */
- originalImageLinkMask;
- /**
- * @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, VideoClip>}
- */
- 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.setOrientation();
- this.createMobileTapControls();
- }
-
- 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.canvasResolutions.search : Gallery.canvasResolutions.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) => {
- if (this.clickedOnAutoplayMenu(event)) {
- return;
- }
- const clickedOnTapControls = event.target.classList.contains("mobile-tap-control");
-
- if (clickedOnTapControls) {
- 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 (event.shiftKey && (this.inGallery || clickedOnAThumb)) {
- this.openPostInNewPage();
- return;
- }
-
- if (this.inGallery) {
- if (Utils.isVideo(this.getSelectedThumb()) && !Utils.onMobileDevice()) {
- return;
- }
- this.exitGallery();
- this.toggleAllVisibility(false);
- return;
- }
-
- if (!clickedOnAThumb) {
- return;
- }
-
- if (Utils.onMobileDevice()) {
- if (!this.enlargeOnClickOnMobile) {
- this.openPostInNewPage(thumb);
- return;
- }
- this.deleteAllRenders();
- }
-
- if (Utils.onMobileDevice()) {
- this.renderImagesAround(thumb);
- }
-
- this.toggleAllVisibility(true);
- this.enterGallery();
- this.showOriginalContent(thumb);
- break;
-
- case Utils.clickCodes.middle:
- event.preventDefault();
-
- if (this.inGallery || (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;
-
- case "Control":
- if (!event.repeat) {
- this.toggleCtrlClickOpenMediaInNewTab(true);
- }
- 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;
-
- default:
- break;
- }
- }, {
- passive: true
- });
- window.addEventListener("keyup", (event) => {
- if (!this.inGallery) {
- return;
- }
-
- switch (event.key) {
- case "Control":
- this.toggleCtrlClickOpenMediaInNewTab(false);
- break;
-
- default:
- break;
- }
- });
- window.addEventListener("blur", () => {
- this.toggleCtrlClickOpenMediaInNewTab(false);
- });
- }
-
- /**
- * @param {MouseEvent | TouchEvent} event
- */
- clickedOnAutoplayMenu(event) {
- const autoplayMenu = document.getElementById("autoplay-menu");
- return autoplayMenu !== null && autoplayMenu.contains(event.target);
- }
-
- 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("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;
- }
-
- if (!this.clickedOnAutoplayMenu(event)) {
- event.preventDefault();
- }
- Gallery.swipeControls.set(event, true);
- }, {
- passive: false
- });
- document.addEventListener("touchend", (event) => {
- if (!this.inGallery ||
- // event.target.classList.contains("mobile-tap-control") ||
- this.clickedOnAutoplayMenu(event)
- ) {
- return;
- }
- event.preventDefault();
- Gallery.swipeControls.set(event, false);
-
- if (Gallery.swipeControls.up) {
- this.autoplayController.showMenu();
- return;
- }
-
- if (Gallery.swipeControls.down) {
- this.exitGallery();
- this.toggleAllVisibility(false);
- return;
- }
-
- if (Utils.isVideo(this.getSelectedThumb())) {
- return;
- }
-
- if (Gallery.swipeControls.left) {
- this.traverseGallery(Gallery.directions.right, false);
- return;
- }
-
- if (Gallery.swipeControls.right) {
- this.traverseGallery(Gallery.directions.left, false);
-
- }
- // this.exitGallery();
- // this.toggleAllVisibility(false);
-
- }, {
- passive: false
- });
-
- window.addEventListener("orientationchange", () => {
- if (this.imageRenderer !== null && this.imageRenderer !== undefined) {
- this.setOrientation();
- }
- }, {
- passive: true
- });
- }
-
- setOrientation() {
- if (!Utils.onMobileDevice()) {
- return;
- }
- const usingLandscapeOrientation = window.screen.orientation.angle === 90;
-
- this.setGifOrientation(usingLandscapeOrientation);
- this.swapMainCanvasDimensions(usingLandscapeOrientation);
- this.swapLowResolutionCanvasDimensions(usingLandscapeOrientation);
- this.redrawCanvasesOnOrientationChange();
- }
-
- /**
- * @param {Boolean} usingLandscapeOrientation
- */
- swapMainCanvasDimensions(usingLandscapeOrientation) {
- this.imageRenderer.postMessage({
- action: "changeCanvasOrientation",
- usingLandscapeOrientation
- });
- }
-
- /**
- * @param {Boolean} usingLandscapeOrientation
- */
- setGifOrientation(usingLandscapeOrientation) {
- const orientationId = "main-orientation";
-
- if (usingLandscapeOrientation) {
- Utils.insertStyleHTML(`
- #original-gif-container, #main-canvas, #low-resolution-canvas {
- height: 100vh !important;
- width: auto !important;
- }
- `, orientationId);
- } else {
- Utils.insertStyleHTML(`
- #original-gif-container, #main-canvas, #low-resolution-canvas {
- width: 100vw !important;
- height: auto !important;
- }
- `, orientationId);
- }
- }
-
- /**
- * @param {Boolean} usingLandscapeOrientation
- */
- swapLowResolutionCanvasDimensions(usingLandscapeOrientation) {
- if (usingLandscapeOrientation === (this.lowResolutionCanvas.width > this.lowResolutionCanvas.height)) {
- return;
- }
- const temp = this.lowResolutionCanvas.height;
-
- this.lowResolutionCanvas.height = this.lowResolutionCanvas.width;
- this.lowResolutionCanvas.width = temp;
- }
-
- redrawCanvasesOnOrientationChange() {
- if (!this.inGallery) {
- return;
- }
- const thumb = this.getSelectedThumb();
-
- if (thumb === undefined || thumb === null) {
- return;
- }
- this.drawLowResolutionCanvas(thumb);
- this.imageRenderer.postMessage(this.getRenderRequest(thumb));
- }
-
- createMobileTapControls() {
- if (!Utils.onMobileDevice()) {
- return;
- }
- const tapControlContainer = document.createElement("div");
- const leftTap = document.createElement("div");
- const rightTap = document.createElement("div");
-
- leftTap.className = "mobile-tap-control";
- rightTap.className = "mobile-tap-control";
- leftTap.id = "left-mobile-tap-control";
- rightTap.id = "right-mobile-tap-control";
- tapControlContainer.appendChild(leftTap);
- tapControlContainer.appendChild(rightTap);
- this.originalContentContainer.appendChild(tapControlContainer);
- Utils.insertStyleHTML(`
- .mobile-tap-control {
- position: fixed;
- top: 50%;
- height: 65vh;
- width: 25vw;
- opacity: 0;
- background: red;
- z-index: 9999;
- color: red;
- transform: translateY(-50%);
- }
-
- #left-mobile-tap-control {
- left: 0;
- }
-
- #right-mobile-tap-control {
- right: 0;
- }
- `);
- this.toggleTapTraversal(false);
- leftTap.ontouchend = () => {
- if (this.inGallery) {
- this.traverseGallery(Gallery.directions.left, false);
- }
- };
- rightTap.ontouchend = () => {
- if (this.inGallery) {
- this.traverseGallery(Gallery.directions.right, false);
- }
- };
- }
-
- /**
- * @param {Boolean} value
- */
- toggleTapTraversal(value) {
- Utils.insertStyleHTML(`
- .mobile-tap-control {
- pointer-events: ${value ? "auto" : "none"};
- }
- `, "tap-traversal");
- }
-
- 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 = "mobile-gallery-checkbox";
- optionText = "Gallery";
- 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 ${Gallery.commonVideoAttributes} active></video>
- </a>
- <img id="original-gif-container"></img>
- <a id="original-content-background-link-mask"></a>
- <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.originalImageLinkMask = document.getElementById("original-content-background-link-mask");
- 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 = Utils.onMobileDevice() ? 320 : 1280;
- this.lowResolutionCanvas.height = Utils.onMobileDevice() ? 180 : 720;
- 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 ${Gallery.commonVideoAttributes}></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);
- }
- });
-
- if (Utils.onMobileDevice()) {
- video.addEventListener("touchend", () => {
- this.toggleVideoControls(true);
- }, {
- passive: true
- });
- }
- }
- }
-
- 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()) {
- return;
- }
- const thumbs = Utils.getAllThumbs();
-
- if (Utils.onSearchPage()) {
- this.renderImages(thumbs.filter(thumb => Utils.isImage(thumb)).slice(0, 50));
- return;
- }
- const animatedThumbs = thumbs
- .slice(0, Gallery.settings.animatedThumbsToUpscaleDiscrete)
- .filter(thumb => !Utils.isImage(thumb));
-
- if (thumbs.length > 0) {
- this.upscaleAnimatedThumbs(animatedThumbs);
- this.renderImagesAround(thumbs[0]);
- }
- }
-
- 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();
- }
-
- /**
- * @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.toggleTapTraversal(true);
- 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.toggleTapTraversal(false);
- this.toggleCursorVisibility(true);
- this.toggleVideoControls(false);
- this.background.style.pointerEvents = "none";
- this.toggleCtrlClickOpenMediaInNewTab(false);
- const thumbIndex = this.getIndexOfThumbUnderCursor();
-
- if (Utils.onMobileDevice()) {
- this.hideOriginalContent();
- this.deleteAllRenders();
- }
-
- if (!Utils.onMobileDevice() && 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();
- 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();
-
- if (Utils.onMobileDevice()) {
- this.toggleVideoControls(false);
- }
- }
-
- /**
- * @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 = "";
- this.gifContainer.style.visibility = "hidden";
- }
-
- /**
- * @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 videoClip = this.videoClips.get(thumb.id);
-
- if (videoClip === undefined) {
- video.ontimeupdate = null;
- return;
- }
- video.ontimeupdate = () => {
- if (video.currentTime < videoClip.start || video.currentTime > videoClip.end) {
- video.removeAttribute("controls");
- video.currentTime = videoClip.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.toggleOriginalGIF(true);
- this.lowResolutionCanvas.style.visibility = "hidden";
- this.mainCanvas.style.visibility = "hidden";
- 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);
- this.toggleOriginalGIF(false);
- }
-
- /**
- * @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.getAdjacentImageThumbsThroughoutAllPages(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[]}
- */
- getAdjacentImageThumbsThroughoutAllPages(initialThumb) {
- return this.getAdjacentSearchResults(
- initialThumb,
- Gallery.settings.maxImagesToRenderAround,
- (post) => {
- return Utils.isImage(post);
- }
- );
- }
-
- /**
- * @param {HTMLElement} initialThumb
- * @param {Number} limit
- * @param {Function} qualifier
- * @returns {HTMLElement[]}
- */
- getAdjacentThumbs(initialThumb, limit, qualifier) {
- 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 (qualifier(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);
- }
- currentThumb = traverseForward ? nextThumb : previousThumb;
- traverseForward = !traverseForward;
-
- if (currentThumb === undefined || discoveredIds.has(currentThumb.id)) {
- break;
- }
- discoveredIds.add(currentThumb.id);
-
- if (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);
-
- 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}
- */
- 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) {
- value = this.background.style.display === "block";
- this.background.style.display = value ? "none" : "block";
- this.originalImageLinkMask.style.display = value ? "none" : "block";
- return;
- }
- this.background.style.display = value ? "block" : "none";
- this.originalImageLinkMask.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 (Utils.onMobileDevice()) {
- if (value) {
- video.setAttribute("controls", "");
- }
- } else {
- video.style.pointerEvents = value ? "auto" : "none";
- }
-
- 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) {
- value = this.gifContainer.style.visibility !== "visible";
- }
- this.gifContainer.style.visibility = value ? "visible" : "hidden";
-
- if (Utils.onMobileDevice()) {
- this.gifContainer.style.zIndex = value ? "9995" : "0";
- }
- }
-
- /**
- * @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) {
- 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() {
- 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() {
- window.addEventListener("postProcess", () => {
- setTimeout(() => {
- let storedVideoClips;
-
- try {
- storedVideoClips = JSON.parse(localStorage.getItem("storedVideoClips") || "{}");
-
- for (const [id, videoClip] of Object.entries(storedVideoClips)) {
- this.videoClips.set(id, new VideoClip(videoClip));
- }
- } catch (error) {
- console.error(error);
- }
- }, 50);
- });
- }
-
- /**
- * @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
- }));
- }
-
- async setupOriginalImageLinkInGallery() {
- const thumb = this.getSelectedThumb();
-
- if (thumb === null || thumb === undefined) {
- return;
- }
- const imageURL = await Utils.getOriginalImageURLWithExtension(thumb);
-
- this.toggleCtrlClickOpenMediaInNewTab(false);
- this.originalImageLinkMask.setAttribute("href", imageURL);
- }
-
- /**
- * @param {Boolean} value
- */
- toggleCtrlClickOpenMediaInNewTab(value) {
- if (!this.inGallery && value) {
- return;
- }
- this.originalImageLinkMask.classList.toggle("active", value);
- }
- }
-
- 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.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
- });
- }
-
- 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: 10,
- tagFetchDelayBeforeFinishedLoading: 100
- };
- 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 parseInt(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("favoritesLoadedFromDatabase", () => {
- this.findTagCategoriesOnPageChange();
- }, {
- 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.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:
- if (event.shiftKey) {
- this.searchForTag(tagName);
- } else {
- this.tagOnClickHelper(tagName, event);
- }
- break;
-
- case Utils.clickCodes.middle:
- this.searchForTag(tagName);
- break;
-
- case Utils.clickCodes.right:
- this.tagOnClickHelper(`-${tagName}`, event);
- break;
-
- default:
- break;
- }
- }
-
- /**
- * @param {String} tagName
- */
- searchForTag(tagName) {
- dispatchEvent(new CustomEvent("searchForTag", {
- detail: tagName
- }));
- }
-
- /**
- * @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, () => {
- this.addTags(tagNames, thumb);
- }, 3);
- 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.saveTagCategoriesCooldown.restart();
- });
- }
-
- /**
- * @param {String[]} tagNames
- * @param {Function} onAllCategoriesFound
- * @param {Number} fetchDelay
- */
- async findTagCategories(tagNames, onAllCategoriesFound, fetchDelay) {
- 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 || Caption.tagFetchDelay);
- }
- }
-
- /**
- * @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(40%);
- 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);
- }
- this.restoreMissingCustomTags();
- };
- database.close();
- };
- }
-
- restoreMissingCustomTags() {
- // const allCustomTags = Array.from(TagModifier.tagModifications.values()).join(" ");
- // const allUniqueCustomTags = new Set(allCustomTags.split(" "));
-
- // Utils.setCustomTags(Array.from(allUniqueCustomTags).join(" "));
- }
-
- 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);
- }
- }
-
- class AwesompleteImplementation {
- static 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;
- }());
-
- static {
- Utils.addStaticInitializer(() => {
- // 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);
- }());
- });
- }
- }
-
- 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();
-
- Utils.postProcess();