Rule34 Favorites Search Gallery

Search, View, and Play Rule34 Favorites (Desktop/Android/iOS)

  1. // ==UserScript==
  2. // @name Rule34 Favorites Search Gallery
  3. // @namespace bruh3396
  4. // @version 1.18.4
  5. // @description Search, View, and Play Rule34 Favorites (Desktop/Android/iOS)
  6. // @author bruh3396
  7. // @compatible Chrome
  8. // @compatible Edge
  9. // @compatible Firefox
  10. // @compatible Safari
  11. // @compatible Opera
  12. // @match https://rule34.xxx/index.php?page=favorites&s=view&id=*
  13. // @match https://rule34.xxx/index.php?page=post&s=list*
  14. // ==/UserScript==
  15.  
  16. class Utils {
  17. static utilitiesHTML = `
  18. <style>
  19. .light-green-gradient {
  20. background: linear-gradient(to bottom, #aae5a4, #89e180);
  21. color: black;
  22. }
  23.  
  24. .dark-green-gradient {
  25. background: linear-gradient(to bottom, #5e715e, #293129);
  26. color: white;
  27. }
  28.  
  29. img {
  30. border: none !important;
  31. }
  32.  
  33. .not-highlightable {
  34. -webkit-touch-callout: none;
  35. -webkit-user-select: none;
  36. -khtml-user-select: none;
  37. -moz-user-select: none;
  38. -ms-user-select: none;
  39. user-select: none;
  40. }
  41.  
  42. input[type=number] {
  43. border: 1px solid #767676;
  44. border-radius: 2px;
  45. }
  46.  
  47. .size-calculation-div {
  48. position: absolute !important;
  49. top: 0;
  50. left: 0;
  51. width: 100%;
  52. height: 100%;
  53. visibility: hidden;
  54. transition: none !important;
  55. transform: scale(1.05, 1.05);
  56. }
  57.  
  58. .number {
  59. white-space: nowrap;
  60. position: relative;
  61. margin-top: 5px;
  62. border: 1px solid;
  63. padding: 0;
  64. border-radius: 20px;
  65. background-color: white;
  66.  
  67. >hold-button,
  68. button {
  69. position: relative;
  70. top: 0;
  71. left: 0;
  72. font-size: inherit;
  73. outline: none;
  74. background: none;
  75. cursor: pointer;
  76. border: none;
  77. margin: 0px 8px;
  78. padding: 0;
  79.  
  80. &::after {
  81. content: '';
  82. position: absolute;
  83. top: 50%;
  84. left: 50%;
  85. transform: translate(-50%, -50%);
  86. width: 200%;
  87. height: 100%;
  88. /* outline: 1px solid greenyellow; */
  89. /* background-color: hotpink; */
  90. }
  91.  
  92. &:hover {
  93. >span {
  94. color: #0075FF;
  95. }
  96.  
  97. }
  98.  
  99. >span {
  100. font-weight: bold;
  101. font-family: Verdana, Geneva, Tahoma, sans-serif;
  102. position: relative;
  103. pointer-events: none;
  104. border: none;
  105. outline: none;
  106. top: 0;
  107. z-index: 5;
  108. font-size: 1.2em !important;
  109. }
  110.  
  111. &.number-arrow-up {
  112. >span {
  113. transition: left .1s ease;
  114. left: 0;
  115. }
  116.  
  117. &:hover>span {
  118. left: 3px;
  119. }
  120. }
  121.  
  122. &.number-arrow-down {
  123. >span {
  124. transition: right .1s ease;
  125. right: 0;
  126. }
  127.  
  128. &:hover>span {
  129. right: 3px;
  130. }
  131. }
  132. }
  133.  
  134. >input[type="number"] {
  135. font-size: inherit;
  136. text-align: center;
  137. width: 2ch;
  138. padding: 0;
  139. margin: 0;
  140. font-weight: bold;
  141. padding: 3px;
  142. background: none;
  143. border: none;
  144.  
  145. &:focus {
  146. outline: none;
  147. }
  148. }
  149.  
  150. >input[type="number"]::-webkit-outer-spin-button,
  151. >input[type="number"]::-webkit-inner-spin-button {
  152. -webkit-appearance: none;
  153. appearance: none;
  154. margin: 0;
  155. }
  156.  
  157. input[type=number] {
  158. appearance: textfield;
  159. -moz-appearance: textfield;
  160. }
  161. }
  162.  
  163. .fullscreen-icon {
  164. position: fixed;
  165. top: 50%;
  166. left: 50%;
  167. transform: translate(-50%, -50%);
  168. z-index: 10010;
  169. pointer-events: none;
  170. width: 30vw;
  171. }
  172.  
  173. input[type="checkbox"] {
  174. accent-color: #0075FF;
  175. }
  176.  
  177. .thumb {
  178. >a {
  179. pointer-events: none;
  180.  
  181. >img {
  182. pointer-events: all;
  183. }
  184. }
  185. }
  186.  
  187. .blink {
  188. animation: blink 0.35s step-start infinite;
  189. }
  190.  
  191. @keyframes blink {
  192. 0% {
  193. opacity: 1;
  194. }
  195.  
  196. 50% {
  197. opacity: 0;
  198. }
  199.  
  200. 100% {
  201. opacity: 1;
  202. }
  203. }
  204. </style>
  205. `;
  206. static localStorageKeys = {
  207. imageExtensions: "imageExtensions"
  208. };
  209. static settings = {
  210. extensionsFoundBeforeSavingCount: 100
  211. };
  212. static favoritesSearchGalleryContainer = Utils.createFavoritesSearchGalleryContainer();
  213. static idsToRemoveOnReloadLocalStorageKey = "recentlyRemovedIds";
  214. static tagBlacklist = Utils.getTagBlacklist();
  215. static preferencesLocalStorageKey = "preferences";
  216. static flags = {
  217. set: false,
  218. onSearchPage: {
  219. set: false,
  220. value: undefined
  221. },
  222. onFavoritesPage: {
  223. set: false,
  224. value: undefined
  225. },
  226. onPostPage: {
  227. set: false,
  228. value: undefined
  229. },
  230. usingFirefox: {
  231. set: false,
  232. value: undefined
  233. },
  234. onMobileDevice: {
  235. set: false,
  236. value: undefined
  237. },
  238. userIsOnTheirOwnFavoritesPage: {
  239. set: false,
  240. value: undefined
  241. },
  242. galleryEnabled: {
  243. set: false,
  244. value: undefined
  245. }
  246. };
  247. static icons = {
  248. 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>",
  249. 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>",
  250. 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>",
  251. 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>",
  252. 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>",
  253. 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>",
  254. 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>",
  255. 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>",
  256. empty: "<button>123</button>",
  257. 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>",
  258. 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>",
  259. 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>",
  260. 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>",
  261. 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>",
  262. 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>"
  263. };
  264. static defaults = {
  265. columnCount: 6,
  266. resultsPerPage: 200
  267. };
  268. static addedFavoriteStatuses = {
  269. error: 0,
  270. alreadyAdded: 1,
  271. notLoggedIn: 2,
  272. success: 3
  273. };
  274. static styles = {
  275. thumbHoverOutline: `
  276. .favorite,
  277. .thumb {
  278. >a,
  279. >span,
  280. >div {
  281. &:hover {
  282. outline: 3px solid #0075FF !important;
  283. }
  284. }
  285. }`,
  286. thumbHoverOutlineDisabled: `
  287. .favorite,
  288. .thumb {
  289. >a,
  290. >span,
  291. >div:not(:has(img.video)) {
  292. &:hover {
  293. outline: none;
  294. }
  295. }
  296. }`,
  297. darkTheme: `
  298. input[type=number] {
  299. background-color: #303030;
  300. color: white;
  301. }
  302.  
  303. .number {
  304. background-color: #303030;
  305.  
  306. >hold-button,
  307. button {
  308. color: white;
  309. }
  310. }
  311.  
  312. #favorites-pagination-container {
  313. >button {
  314. border: 1px solid white !important;
  315. color: white !important;
  316. }
  317. }
  318. `
  319. };
  320. static typeableInputs = new Set([
  321. "color",
  322. "email",
  323. "number",
  324. "password",
  325. "search",
  326. "tel",
  327. "text",
  328. "url",
  329. "datetime"
  330. ]);
  331. static clickCodes = {
  332. left: 0,
  333. middle: 1,
  334. right: 2
  335. };
  336. static customTags = Utils.loadCustomTags();
  337. static favoriteItemClassName = "favorite";
  338. static imageExtensions = Utils.loadDiscoveredImageExtensions();
  339. /**
  340. * @type {Cooldown}
  341. */
  342. static imageExtensionAssignmentCooldown;
  343. static recentlyDiscoveredImageExtensionCount = 0;
  344. static extensionDecodings = {
  345. 0: "jpg",
  346. 1: "png",
  347. 2: "jpeg",
  348. 3: "gif"
  349. };
  350. static extensionEncodings = {
  351. "jpg": 0,
  352. "png": 1,
  353. "jpeg": 2,
  354. "gif": 3
  355. };
  356. /**
  357. * @type {Function[]}
  358. */
  359. static staticInitializers = [];
  360.  
  361. /**
  362. * @type {Boolean}
  363. */
  364. static get disabled() {
  365. if (Utils.onPostPage()) {
  366. return true;
  367. }
  368.  
  369. if (Utils.onFavoritesPage()) {
  370. return false;
  371. }
  372. const enabledOnSearchPages = Utils.getPreference("enableOnSearchPages", false);
  373. return !enabledOnSearchPages;
  374. }
  375.  
  376. /**
  377. * @type {Boolean}
  378. */
  379. static get enabled() {
  380. return !Utils.disabled;
  381. }
  382.  
  383. static initialize() {
  384. if (Utils.disabled) {
  385. throw new Error("Favorites Search Gallery disabled");
  386. }
  387. Utils.invokeStaticInitializers();
  388. Utils.removeUnusedScripts();
  389. Utils.insertCommonStyleHTML();
  390. Utils.setupCustomWebComponents();
  391. Utils.toggleFancyImageHovering(true);
  392. Utils.setTheme();
  393. Utils.prepareSearchPage();
  394. Utils.prefetchAdjacentSearchPages();
  395. Utils.setupOriginalImageLinksOnSearchPage();
  396. Utils.initializeImageExtensionAssignmentCooldown();
  397. }
  398.  
  399. static postProcess() {
  400. dispatchEvent(new Event("postProcess"));
  401. }
  402.  
  403. /**
  404. * @param {String} key
  405. * @param {any} value
  406. */
  407. static setCookie(key, value) {
  408. let cookieString = `${key}=${value || ""}`;
  409. const expirationDate = new Date();
  410.  
  411. expirationDate.setFullYear(expirationDate.getFullYear() + 1);
  412. cookieString += `; expires=${expirationDate.toUTCString()}`;
  413. cookieString += "; path=/";
  414. document.cookie = cookieString;
  415. }
  416.  
  417. /**
  418. * @param {String} key
  419. * @param {any} defaultValue
  420. * @returns {String | null}
  421. */
  422. static getCookie(key, defaultValue) {
  423. const nameEquation = `${key}=`;
  424. const cookies = document.cookie.split(";").map(cookie => cookie.trimStart());
  425.  
  426. for (const cookie of cookies) {
  427. if (cookie.startsWith(nameEquation)) {
  428. return cookie.substring(nameEquation.length, cookie.length);
  429. }
  430. }
  431. return defaultValue === undefined ? null : defaultValue;
  432. }
  433.  
  434. /**
  435. * @param {String} key
  436. * @param {any} value
  437. */
  438. static setPreference(key, value) {
  439. const preferences = JSON.parse(localStorage.getItem(Utils.preferencesLocalStorageKey) || "{}");
  440.  
  441. preferences[key] = value;
  442. localStorage.setItem(Utils.preferencesLocalStorageKey, JSON.stringify(preferences));
  443. }
  444.  
  445. /**
  446. * @param {String} key
  447. * @param {any} defaultValue
  448. * @returns {String | null}
  449. */
  450. static getPreference(key, defaultValue) {
  451. const preferences = JSON.parse(localStorage.getItem(Utils.preferencesLocalStorageKey) || "{}");
  452. const preference = preferences[key];
  453.  
  454. if (preference === undefined) {
  455. return defaultValue === undefined ? null : defaultValue;
  456. }
  457. return preference;
  458. }
  459.  
  460. /**
  461. * @returns {String | null}
  462. */
  463. static getUserId() {
  464. return Utils.getCookie("user_id");
  465. }
  466.  
  467. /**
  468. * @returns {String | null}
  469. */
  470. static getFavoritesPageId() {
  471. const match = (/(?:&|\?)id=(\d+)/).exec(window.location.href);
  472. return match ? match[1] : null;
  473. }
  474.  
  475. /**
  476. * @returns {Boolean}
  477. */
  478. static userIsOnTheirOwnFavoritesPage() {
  479. if (!Utils.flags.userIsOnTheirOwnFavoritesPage.set) {
  480. Utils.flags.userIsOnTheirOwnFavoritesPage.value = Utils.getUserId() === Utils.getFavoritesPageId();
  481. Utils.flags.userIsOnTheirOwnFavoritesPage.set = true;
  482. }
  483. return Utils.flags.userIsOnTheirOwnFavoritesPage.value;
  484. }
  485.  
  486. /**
  487. * @param {Number} value
  488. * @param {Number} min
  489. * @param {Number} max
  490. * @returns {Number}
  491. */
  492. static clamp(value, min, max) {
  493. if (value <= min) {
  494. return min;
  495. } else if (value >= max) {
  496. return max;
  497. }
  498. return value;
  499. }
  500.  
  501. /**
  502. * @param {Number} milliseconds
  503. * @returns
  504. */
  505. static sleep(milliseconds) {
  506. return new Promise(resolve => setTimeout(resolve, milliseconds));
  507. }
  508.  
  509. /**
  510. * @param {Boolean} value
  511. */
  512. static forceHideCaptions(value) {
  513. for (const caption of document.getElementsByClassName("caption")) {
  514. if (value) {
  515. caption.classList.add("remove");
  516. caption.classList.add("inactive");
  517. } else {
  518. caption.classList.remove("remove");
  519. }
  520. }
  521. }
  522.  
  523. /**
  524. * @param {HTMLElement} thumb
  525. * @returns {String | null}
  526. */
  527. static getRemoveFavoriteButtonFromThumb(thumb) {
  528. return thumb.querySelector(".remove-favorite-button");
  529. }
  530.  
  531. /**
  532. * @param {HTMLElement} thumb
  533. * @returns {String | null}
  534. */
  535. static getAddFavoriteButtonFromThumb(thumb) {
  536. return thumb.querySelector(".add-favorite-button");
  537. }
  538.  
  539. /**
  540. * @param {HTMLImageElement} image
  541. */
  542. static removeTitleFromImage(image) {
  543. if (image.hasAttribute("title")) {
  544. image.setAttribute("tags", image.title);
  545. image.removeAttribute("title");
  546. }
  547. }
  548.  
  549. /**
  550. * @param {HTMLImageElement} image
  551. * @returns {HTMLElement}
  552. */
  553. static getThumbFromImage(image) {
  554. const className = Utils.onSearchPage() ? "thumb" : Utils.favoriteItemClassName;
  555. return image.closest(`.${className}`);
  556. }
  557.  
  558. /**
  559. * @param {HTMLElement} thumb
  560. * @returns {HTMLImageElement}
  561. */
  562. static getImageFromThumb(thumb) {
  563. return thumb.querySelector("img");
  564. }
  565.  
  566. /**
  567. * @returns {HTMLElement[]}
  568. */
  569. static getAllThumbs() {
  570. const className = Utils.onSearchPage() ? "thumb" : Utils.favoriteItemClassName;
  571. return Array.from(document.getElementsByClassName(className));
  572. }
  573.  
  574. /**
  575. * @param {HTMLElement} thumb
  576. * @returns {String}
  577. */
  578. static getOriginalImageURLFromThumb(thumb) {
  579. return Utils.getOriginalImageURL(Utils.getImageFromThumb(thumb).src);
  580. }
  581.  
  582. /**
  583. * @param {String} thumbURL
  584. * @returns {String}
  585. */
  586. static getOriginalImageURL(thumbURL) {
  587. return thumbURL
  588. .replace("thumbnails", "/images")
  589. .replace("thumbnail_", "")
  590. .replace("us.rule34", "rule34");
  591. }
  592.  
  593. /**
  594. * @param {String} imageURL
  595. * @returns {String}
  596. */
  597. static getExtensionFromImageURL(imageURL) {
  598. try {
  599. return (/\.(png|jpg|jpeg|gif|mp4)/g).exec(imageURL)[1];
  600.  
  601. } catch (error) {
  602. return "jpg";
  603. }
  604. }
  605.  
  606. /**
  607. * @param {String} originalImageURL
  608. * @returns {String}
  609. */
  610. static getThumbURL(originalImageURL) {
  611. return originalImageURL
  612. .replace(/\/images\/\/(\d+)\//, "thumbnails/$1/thumbnail_")
  613. .replace(/(?:gif|jpeg|png)/, "jpg")
  614. .replace("us.rule34", "rule34");
  615. }
  616.  
  617. /**
  618. * @param {HTMLElement | Post} thumb
  619. * @returns {Set.<String>}
  620. */
  621. static getTagsFromThumb(thumb) {
  622. if (Utils.onSearchPage()) {
  623. const image = Utils.getImageFromThumb(thumb);
  624. const tags = image.hasAttribute("tags") ? image.getAttribute("tags") : image.title;
  625. return Utils.convertToTagSet(tags);
  626. }
  627. const post = Post.allPosts.get(thumb.id);
  628. return post === undefined ? new Set() : new Set(post.tagSet);
  629. }
  630.  
  631. /**
  632. * @param {String} tag
  633. * @param {Set.<String>} tags
  634. * @returns
  635. */
  636. static includesTag(tag, tags) {
  637. return tags.has(tag);
  638. }
  639.  
  640. /**
  641. * @param {HTMLElement | Post} thumb
  642. * @returns {Boolean}
  643. */
  644. static isVideo(thumb) {
  645. const tags = Utils.getTagsFromThumb(thumb);
  646. return tags.has("video") || tags.has("mp4");
  647. }
  648.  
  649. /**
  650. * @param {HTMLElement | Post} thumb
  651. * @returns {Boolean}
  652. */
  653. static isGif(thumb) {
  654. if (Utils.isVideo(thumb)) {
  655. return false;
  656. }
  657. const tags = Utils.getTagsFromThumb(thumb);
  658. return tags.has("gif") || tags.has("animated") || tags.has("animated_png") || Utils.hasGifAttribute(thumb);
  659. }
  660.  
  661. /**
  662. * @param {HTMLElement | Post} thumb
  663. * @returns {Boolean}
  664. */
  665. static hasGifAttribute(thumb) {
  666. if (thumb instanceof Post) {
  667. return false;
  668. }
  669. return Utils.getImageFromThumb(thumb).hasAttribute("gif");
  670. }
  671.  
  672. /**
  673. * @param {HTMLElement | Post} thumb
  674. * @returns {Boolean}
  675. */
  676. static isImage(thumb) {
  677. return !Utils.isVideo(thumb) && !Utils.isGif(thumb);
  678. }
  679.  
  680. /**
  681. * @param {Number} maximum
  682. * @returns {Number}
  683. */
  684. static getRandomInteger(maximum) {
  685. return Math.floor(Math.random() * maximum);
  686. }
  687.  
  688. /**
  689. * @param {any[]} array
  690. * @returns {any[]}
  691. */
  692. static shuffleArray(array) {
  693. let maxIndex = array.length;
  694. let randomIndex;
  695.  
  696. while (maxIndex > 0) {
  697. randomIndex = Utils.getRandomInteger(maxIndex);
  698. maxIndex -= 1;
  699. [
  700. array[maxIndex],
  701. array[randomIndex]
  702. ] = [
  703. array[randomIndex],
  704. array[maxIndex]
  705. ];
  706. }
  707. return array;
  708. }
  709.  
  710. /**
  711. * @param {String} tags
  712. * @returns {String}
  713. */
  714. static negateTags(tags) {
  715. return tags.replace(/(\S+)/g, "-$1");
  716. }
  717.  
  718. /**
  719. * @param {HTMLInputElement | HTMLTextAreaElement} input
  720. * @returns {HTMLDivElement | null}
  721. */
  722. static getAwesompleteFromInput(input) {
  723. const awesomplete = input.parentElement;
  724.  
  725. if (awesomplete === null || awesomplete.className !== "awesomplete") {
  726. return null;
  727. }
  728. return awesomplete;
  729. }
  730.  
  731. /**
  732. * @param {HTMLInputElement | HTMLTextAreaElement} input
  733. * @returns {Boolean}
  734. */
  735. static awesompleteIsVisible(input) {
  736. const awesomplete = Utils.getAwesompleteFromInput(input);
  737.  
  738. if (awesomplete === null) {
  739. return false;
  740. }
  741. const awesompleteSuggestions = awesomplete.querySelector("ul");
  742. return awesompleteSuggestions !== null && !awesompleteSuggestions.hasAttribute("hidden");
  743. }
  744.  
  745. /**
  746. *
  747. * @param {HTMLInputElement | HTMLTextAreaElement} input
  748. * @returns
  749. */
  750. static awesompleteIsUnselected(input) {
  751. const awesomplete = Utils.getAwesompleteFromInput(input);
  752.  
  753. if (awesomplete === null) {
  754. return true;
  755. }
  756.  
  757. if (!Utils.awesompleteIsVisible(input)) {
  758. return true;
  759. }
  760. const searchSuggestions = Array.from(awesomplete.querySelectorAll("li"));
  761.  
  762. if (searchSuggestions.length === 0) {
  763. return true;
  764. }
  765. const somethingIsSelected = searchSuggestions.map(li => li.getAttribute("aria-selected"))
  766. .some(element => element === true || element === "true");
  767. return !somethingIsSelected;
  768. }
  769.  
  770. /**
  771. * @param {HTMLInputElement | HTMLTextAreaElement} input
  772. * @returns
  773. */
  774. static clearAwesompleteSelection(input) {
  775. const awesomplete = input.parentElement;
  776.  
  777. if (awesomplete === null) {
  778. return;
  779. }
  780. const searchSuggestions = Array.from(awesomplete.querySelectorAll("li"));
  781.  
  782. if (searchSuggestions.length === 0) {
  783. return;
  784. }
  785.  
  786. for (const li of searchSuggestions) {
  787. li.setAttribute("aria-selected", false);
  788. }
  789. }
  790.  
  791. /**
  792. * @param {String} optionId
  793. * @param {String} optionText
  794. * @param {String} optionTitle
  795. * @param {Boolean} optionIsChecked
  796. * @param {Function} onOptionChanged
  797. * @param {Boolean} optionIsVisible
  798. * @param {String} optionHint
  799. * @returns {HTMLElement | null}
  800. */
  801. static createFavoritesOption(optionId, optionText, optionTitle, optionIsChecked, onOptionChanged, optionIsVisible, optionHint = "") {
  802. const id = Utils.onMobileDevice() ? "favorite-options" : "dynamic-favorite-options";
  803. const placeToInsert = document.getElementById(id);
  804. const checkboxId = `${optionId}-checkbox`;
  805.  
  806. if (placeToInsert === null) {
  807. return null;
  808. }
  809.  
  810. if (optionIsVisible === undefined || optionIsVisible) {
  811. optionIsVisible = "block";
  812. } else {
  813. optionIsVisible = "none";
  814. }
  815. placeToInsert.insertAdjacentHTML("beforeend", `
  816. <div id="${optionId}" style="display: ${optionIsVisible}">
  817. <label class="checkbox" title="${optionTitle}">
  818. <input id="${checkboxId}" type="checkbox"><span> ${optionText}</span><span class="option-hint"> ${optionHint}</span></label>
  819. </div>
  820. `);
  821. const newOptionsCheckbox = document.getElementById(checkboxId);
  822.  
  823. newOptionsCheckbox.checked = optionIsChecked;
  824. newOptionsCheckbox.onchange = onOptionChanged;
  825. return document.getElementById(optionId);
  826. }
  827.  
  828. /**
  829. * @returns {Boolean}
  830. */
  831. static onSearchPage() {
  832. if (!Utils.flags.onSearchPage.set) {
  833. Utils.flags.onSearchPage.value = location.href.includes("page=post&s=list");
  834. Utils.flags.onSearchPage.set = true;
  835. }
  836. return Utils.flags.onSearchPage.value;
  837. }
  838.  
  839. /**
  840. * @returns {Boolean}
  841. */
  842. static onFavoritesPage() {
  843. if (!Utils.flags.onFavoritesPage.set) {
  844. Utils.flags.onFavoritesPage.value = location.href.includes("page=favorites");
  845. Utils.flags.onFavoritesPage.set = true;
  846. }
  847. return Utils.flags.onFavoritesPage.value;
  848. }
  849.  
  850. /**
  851. * @returns {Boolean}
  852. */
  853. static onPostPage() {
  854. if (!Utils.flags.onPostPage.set) {
  855. Utils.flags.onPostPage.value = location.href.includes("page=post&s=view");
  856. Utils.flags.onPostPage.set = true;
  857. }
  858. return Utils.flags.onPostPage.value;
  859. }
  860.  
  861. /**
  862. * @returns {String[]}
  863. */
  864. static getIdsToDeleteOnReload() {
  865. return JSON.parse(localStorage.getItem(Utils.idsToRemoveOnReloadLocalStorageKey)) || [];
  866. }
  867.  
  868. /**
  869. * @param {String} postId
  870. */
  871. static setIdToBeRemovedOnReload(postId) {
  872. const idsToRemoveOnReload = Utils.getIdsToDeleteOnReload();
  873.  
  874. idsToRemoveOnReload.push(postId);
  875. localStorage.setItem(Utils.idsToRemoveOnReloadLocalStorageKey, JSON.stringify(idsToRemoveOnReload));
  876. }
  877.  
  878. static clearIdsToDeleteOnReload() {
  879. localStorage.removeItem(Utils.idsToRemoveOnReloadLocalStorageKey);
  880. }
  881.  
  882. /**
  883. * @param {String} html
  884. * @param {String} id
  885. */
  886. static insertStyleHTML(html, id) {
  887. const style = document.createElement("style");
  888.  
  889. style.textContent = html.replace("<style>", "").replace("</style>", "");
  890.  
  891. if (id !== undefined) {
  892. id += "-fsg-style";
  893. const oldStyle = document.getElementById(id);
  894.  
  895. if (oldStyle !== null) {
  896. oldStyle.remove();
  897. }
  898. style.id = id;
  899. }
  900. document.head.appendChild(style);
  901. }
  902.  
  903. static getTagDistribution() {
  904. const images = Utils.getAllThumbs().map(thumb => Utils.getImageFromThumb(thumb));
  905. const tagOccurrences = {};
  906.  
  907. images.forEach((image) => {
  908. const tags = image.getAttribute("tags").replace(/ \d+$/, "").split(" ");
  909.  
  910. tags.forEach((tag) => {
  911. const occurrences = tagOccurrences[tag];
  912.  
  913. tagOccurrences[tag] = occurrences === undefined ? 1 : occurrences + 1;
  914. });
  915. });
  916. const sortedTagOccurrences = Utils.sortObjectByValues(tagOccurrences);
  917. let result = "";
  918. let i = 0;
  919. const max = 50;
  920.  
  921. sortedTagOccurrences.forEach(item => {
  922. if (i < max) {
  923. result += `${item.key}: ${item.value}\n`;
  924. }
  925. i += 1;
  926. });
  927. }
  928.  
  929. /**
  930. * @param {{key: any, value: any}} obj
  931. * @returns {{key: any, value: any}}
  932. */
  933. static sortObjectByValues(obj) {
  934. const sortable = Object.entries(obj);
  935.  
  936. sortable.sort((a, b) => b[1] - a[1]);
  937. return sortable.map(item => ({
  938. key: item[0],
  939. value: item[1]
  940. }));
  941. }
  942.  
  943. static insertCommonStyleHTML() {
  944. Utils.insertStyleHTML(Utils.utilitiesHTML, "common");
  945. Utils.toggleThumbHoverOutlines(false);
  946. setTimeout(() => {
  947. if (Utils.onSearchPage()) {
  948. Utils.removeInlineImgStyles();
  949. }
  950. Utils.configureVideoOutlines();
  951. }, 100);
  952. }
  953.  
  954. /**
  955. * @param {Boolean} value
  956. */
  957. static toggleFancyImageHovering(value) {
  958. if (Utils.onMobileDevice() || Utils.onSearchPage()) {
  959. value = false;
  960. }
  961. let html = "";
  962.  
  963. if (value) {
  964. html = `
  965. #favorites-search-gallery-content {
  966. padding: 40px 40px 30px !important;
  967. grid-gap: 1cqw !important;
  968. }
  969.  
  970. .favorite,
  971. .thumb {
  972. >a,
  973. >span,
  974. >div {
  975. box-shadow: 0 1px 2px rgba(0,0,0,0.15);
  976. transition: transform 0.2s ease-in-out;
  977. position: relative;
  978.  
  979. &::after {
  980. content: '';
  981. position: absolute;
  982. z-index: -1;
  983. width: 100%;
  984. height: 100%;
  985. opacity: 0;
  986. top: 0;
  987. left: 0;
  988. border-radius: 5px;
  989. box-shadow: 5px 10px 15px rgba(0,0,0,0.45);
  990. transition: opacity 0.3s ease-in-out;
  991. }
  992.  
  993. &:hover {
  994. outline: none !important;
  995. transform: scale(1.05, 1.05);
  996. z-index: 10;
  997.  
  998. img {
  999. outline: none !important;
  1000. }
  1001.  
  1002. &::after {
  1003. opacity: 1;
  1004. }
  1005. }
  1006. }
  1007. }
  1008. `;
  1009. }
  1010. Utils.insertStyleHTML(html, "fancy-image-hovering");
  1011. }
  1012.  
  1013. static configureVideoOutlines() {
  1014. const size = Utils.onMobileDevice() ? 2 : 3;
  1015. const videoSelector = Utils.onFavoritesPage() ? "&:has(img.video)" : ">img.video";
  1016. const gifSelector = Utils.onFavoritesPage() ? "&:has(img.gif)" : ">img.gif";
  1017.  
  1018. Utils.insertStyleHTML(`
  1019. .favorite, .thumb {
  1020.  
  1021. >a,
  1022. >div {
  1023. ${videoSelector} {
  1024. outline: ${size}px solid blue;
  1025. }
  1026.  
  1027. ${gifSelector} {
  1028. outline: 2px solid hotpink;
  1029. }
  1030. }
  1031. }
  1032. `, "video-gif-borders");
  1033. }
  1034.  
  1035. static removeInlineImgStyles() {
  1036. for (const image of document.getElementsByTagName("img")) {
  1037. image.removeAttribute("style");
  1038. }
  1039. }
  1040.  
  1041. static setTheme() {
  1042. window.addEventListener("postProcess", () => {
  1043. Utils.toggleDarkTheme(Utils.usingDarkTheme());
  1044. });
  1045. }
  1046.  
  1047. /**
  1048. * @param {Boolean} value
  1049. */
  1050. static toggleDarkTheme(value) {
  1051. Utils.insertStyleHTML(value ? Utils.styles.darkTheme : "", "dark-theme");
  1052. Utils.toggleDarkStyleSheet(value);
  1053. const currentTheme = value ? "light-green-gradient" : "dark-green-gradient";
  1054. const targetTheme = value ? "dark-green-gradient" : "light-green-gradient";
  1055.  
  1056. for (const element of document.querySelectorAll(`.${currentTheme}`)) {
  1057. element.classList.remove(currentTheme);
  1058. element.classList.add(targetTheme);
  1059. }
  1060. this.setCookie("theme", value ? "dark" : "light");
  1061. }
  1062.  
  1063. static toggleDarkStyleSheet(value) {
  1064. const platform = Utils.onMobileDevice() ? "mobile" : "desktop";
  1065. const darkSuffix = value ? "-dark" : "";
  1066.  
  1067. Utils.setStyleSheet(`https://rule34.xxx//css/${platform}${darkSuffix}.css?44`);
  1068. }
  1069.  
  1070. /**
  1071. * @param {String} url
  1072. */
  1073. static setStyleSheet(url) {
  1074. const styleSheet = this.getMainStyleSheet();
  1075.  
  1076. if (styleSheet !== null && styleSheet !== undefined) {
  1077. styleSheet.href = url;
  1078. }
  1079. }
  1080.  
  1081. /**
  1082. * @returns {HTMLLinkElement}
  1083. */
  1084. static getMainStyleSheet() {
  1085. return Array.from(document.querySelectorAll("link")).filter(link => link.rel === "stylesheet")[0];
  1086. }
  1087.  
  1088. /**
  1089. * @param {String} content
  1090. * @returns {Blob | MediaSource}
  1091. */
  1092. static getWorkerURL(content) {
  1093. return URL.createObjectURL(new Blob([content], {
  1094. type: "text/javascript"
  1095. }));
  1096. }
  1097.  
  1098. static prefetchAdjacentSearchPages() {
  1099. if (!Utils.onSearchPage()) {
  1100. return;
  1101. }
  1102. const id = "search-page-prefetch";
  1103. const alreadyPrefetched = document.getElementById(id) !== null;
  1104.  
  1105. if (alreadyPrefetched) {
  1106. return;
  1107. }
  1108. const container = document.createElement("div");
  1109.  
  1110. try {
  1111. const currentPage = document.getElementById("paginator").children[0].querySelector("b");
  1112.  
  1113. for (const sibling of [currentPage.previousElementSibling, currentPage.nextElementSibling]) {
  1114. if (sibling !== null && sibling.tagName.toLowerCase() === "a") {
  1115. container.appendChild(Utils.createPrefetchLink(sibling.href));
  1116. }
  1117. }
  1118. container.id = "search-page-prefetch";
  1119. document.head.appendChild(container);
  1120. } catch (error) {
  1121. console.error(error);
  1122. }
  1123. }
  1124.  
  1125. /**
  1126. * @param {String} url
  1127. * @returns {HTMLLinkElement}
  1128. */
  1129. static createPrefetchLink(url) {
  1130. const link = document.createElement("link");
  1131.  
  1132. link.rel = "prefetch";
  1133. link.href = url;
  1134. return link;
  1135.  
  1136. }
  1137.  
  1138. /**
  1139. * @returns {String}
  1140. */
  1141. static getTagBlacklist() {
  1142. let tags = Utils.getCookie("tag_blacklist", "");
  1143.  
  1144. for (let i = 0; i < 3; i += 1) {
  1145. tags = decodeURIComponent(tags).replace(/(?:^| )-/, "");
  1146. }
  1147. return tags;
  1148. }
  1149.  
  1150. /**
  1151. * @returns {Boolean}
  1152. */
  1153. static galleryEnabled() {
  1154. if (!Utils.flags.galleryEnabled.set) {
  1155. Utils.flags.galleryEnabled.value = document.getElementById("gallery-container") !== null;
  1156. Utils.flags.galleryEnabled.set = true;
  1157. }
  1158. return Utils.flags.galleryEnabled.value;
  1159. }
  1160.  
  1161. /**
  1162. * @param {String} word
  1163. * @returns {String}
  1164. */
  1165. static capitalize(word) {
  1166. return word.charAt(0).toUpperCase() + word.slice(1);
  1167. }
  1168.  
  1169. /**
  1170. * @param {Number} number
  1171. * @returns {Number}
  1172. */
  1173. static roundToTwoDecimalPlaces(number) {
  1174. return Math.round((number + Number.EPSILON) * 100) / 100;
  1175. }
  1176.  
  1177. /**
  1178. * @param {Number} n
  1179. * @param {Number} number
  1180. */
  1181. static roundToNDecimalPlaces(n, number) {
  1182. const x = 10 ** n;
  1183. return Math.round((number + Number.EPSILON) * x) / x;
  1184. }
  1185.  
  1186. /**
  1187. * @returns {Boolean}
  1188. */
  1189. static usingDarkTheme() {
  1190. return Utils.getCookie("theme", "") === "dark";
  1191. }
  1192.  
  1193. /**
  1194. * @param {Event} event
  1195. * @returns {Boolean}
  1196. */
  1197. static enteredOverCaptionTag(event) {
  1198. return event.relatedTarget !== null && event.relatedTarget.classList.contains("caption-tag");
  1199. }
  1200.  
  1201. /**
  1202. * @param {String[]} postId
  1203. * @param {Boolean} endingAnimation
  1204. * @param {Boolean} smoothTransition
  1205. */
  1206. static scrollToThumb(postId, endingAnimation, smoothTransition) {
  1207. const element = document.getElementById(postId);
  1208. const elementIsNotAThumb = element === null || (!element.classList.contains("thumb") && !element.classList.contains(Utils.favoriteItemClassName));
  1209.  
  1210. if (elementIsNotAThumb) {
  1211. return;
  1212. }
  1213. const rect = element.getBoundingClientRect();
  1214. const menu = document.getElementById("favorites-search-gallery-menu");
  1215. const favoritesSearchHeight = menu === null ? 0 : menu.getBoundingClientRect().height;
  1216. let top = rect.top + window.scrollY + (rect.height / 2) - (window.innerHeight / 2) - (favoritesSearchHeight / 2);
  1217.  
  1218. if (Utils.onMobileDevice()) {
  1219. top = Math.max(1, top);
  1220. }
  1221. window.scroll({
  1222. top,
  1223. behavior: smoothTransition ? "smooth" : "instant"
  1224. });
  1225.  
  1226. if (!endingAnimation) {
  1227. return;
  1228. }
  1229. const image = Utils.getImageFromThumb(element);
  1230.  
  1231. image.classList.add("found");
  1232. setTimeout(() => {
  1233. image.classList.remove("found");
  1234. }, 2000);
  1235. }
  1236.  
  1237. /**
  1238. * @param {HTMLElement} thumb
  1239. */
  1240. static assignContentType(thumb) {
  1241. const image = Utils.getImageFromThumb(thumb);
  1242. const tagAttribute = image.hasAttribute("tags") ? "tags" : "title";
  1243. const tags = image.getAttribute(tagAttribute);
  1244.  
  1245. Utils.setContentType(image, Utils.getContentType(tags));
  1246. }
  1247.  
  1248. /**
  1249. * @param {HTMLImageElement} image
  1250. * @param {String} type
  1251. */
  1252. static setContentType(image, type) {
  1253. image.classList.remove("image");
  1254. image.classList.remove("gif");
  1255. image.classList.remove("video");
  1256. image.classList.add(type);
  1257. }
  1258.  
  1259. /**
  1260. * @param {String} tags
  1261. * @returns {String}
  1262. */
  1263. static getContentType(tags) {
  1264. tags += " ";
  1265. const hasVideoTag = (/(?:^|\s)video(?:$|\s)/).test(tags);
  1266. const hasAnimatedTag = (/(?:^|\s)animated(?:$|\s)/).test(tags);
  1267. const isAnimated = hasAnimatedTag || hasVideoTag;
  1268. const isAGif = hasAnimatedTag && !hasVideoTag;
  1269. return isAGif ? "gif" : isAnimated ? "video" : "image";
  1270. }
  1271.  
  1272. static correctMisspelledTags(tags) {
  1273. if ((/vide(?:\s|$)/).test(tags)) {
  1274. tags += " video";
  1275. }
  1276. return tags;
  1277. }
  1278.  
  1279. /**
  1280. * @param {String} searchQuery
  1281. * @returns {{orGroups: String[][], remainingSearchTags: String[]}}
  1282. */
  1283. static extractTagGroups(searchQuery) {
  1284. searchQuery = searchQuery.toLowerCase();
  1285. const orRegex = /(?:^|\s+)\(\s+((?:\S+)(?:(?:\s+~\s+)\S+)*)\s+\)/g;
  1286. const orGroups = Array.from(Utils.removeExtraWhiteSpace(searchQuery)
  1287. .matchAll(orRegex))
  1288. .map((orGroup) => orGroup[1].split(" ~ "));
  1289. const remainingSearchTags = Utils.removeExtraWhiteSpace(searchQuery
  1290. .replace(orRegex, ""))
  1291. .split(" ")
  1292. .filter((searchTag) => searchTag !== "");
  1293. return {
  1294. orGroups,
  1295. remainingSearchTags
  1296. };
  1297. }
  1298.  
  1299. /**
  1300. * @param {String} string
  1301. * @returns {String}
  1302. */
  1303. static removeExtraWhiteSpace(string) {
  1304. return string.trim().replace(/\s\s+/g, " ");
  1305. }
  1306.  
  1307. /**
  1308. * @param {String} string
  1309. * @param {String} replacement
  1310. * @returns {String}
  1311. */
  1312. static replaceLineBreaks(string, replacement = "") {
  1313. return string.replace(/(\r\n|\n|\r)/gm, replacement);
  1314. }
  1315.  
  1316. /**
  1317. *
  1318. * @param {HTMLImageElement} image
  1319. * @returns {Boolean}
  1320. */
  1321. static imageIsLoaded(image) {
  1322. return image.complete || image.naturalWidth !== 0;
  1323. }
  1324.  
  1325. /**
  1326. * @returns {Boolean}
  1327. */
  1328. static usingFirefox() {
  1329. if (!Utils.flags.usingFirefox.set) {
  1330. Utils.flags.usingFirefox.value = navigator.userAgent.toLowerCase().includes("firefox");
  1331. Utils.flags.usingFirefox.set = true;
  1332. }
  1333. return Utils.flags.usingFirefox.value;
  1334. }
  1335.  
  1336. /**
  1337. * @returns {Boolean}
  1338. */
  1339. static onMobileDevice() {
  1340. if (!Utils.flags.onMobileDevice.set) {
  1341. Utils.flags.onMobileDevice.value = (/iPhone|iPad|iPod|Android/i).test(navigator.userAgent);
  1342. Utils.flags.onMobileDevice.set = true;
  1343. }
  1344. return Utils.flags.onMobileDevice.value;
  1345. }
  1346.  
  1347. /**
  1348. * @returns {Number}
  1349. */
  1350. static getPerformanceProfile() {
  1351. return parseInt(Utils.getPreference("performanceProfile", 0));
  1352. }
  1353.  
  1354. /**
  1355. * @param {String} tagName
  1356. * @returns {Promise.<Boolean>}
  1357. */
  1358. static isOfficialTag(tagName) {
  1359. const tagPageURL = `https://rule34.xxx/index.php?page=tags&s=list&tags=${tagName}`;
  1360. return fetch(tagPageURL)
  1361. .then((response) => {
  1362. if (response.ok) {
  1363. return response.text();
  1364. }
  1365. throw new Error(response.statusText);
  1366. })
  1367. .then((html) => {
  1368. const dom = new DOMParser().parseFromString(html, "text/html");
  1369. const columnOfFirstRow = dom.getElementsByClassName("highlightable")[0].getElementsByTagName("td");
  1370. return columnOfFirstRow.length === 3;
  1371. })
  1372. .catch((error) => {
  1373. console.error(error);
  1374. return true;
  1375. });
  1376. }
  1377.  
  1378. /**
  1379. * @param {String} searchQuery
  1380. */
  1381. static openSearchPage(searchQuery) {
  1382. window.open(`https://rule34.xxx/index.php?page=post&s=list&tags=${encodeURIComponent(searchQuery)}`);
  1383. }
  1384.  
  1385. /**
  1386. * @param {Map} map
  1387. * @returns {Object}
  1388. */
  1389. static mapToObject(map) {
  1390. return Array.from(map).reduce((object, [key, value]) => {
  1391. object[key] = value;
  1392. return object;
  1393. }, {});
  1394. }
  1395.  
  1396. /**
  1397. * @param {Object} object
  1398. * @returns {Map}
  1399. */
  1400. static objectToMap(object) {
  1401. return new Map(Object.entries(object));
  1402. }
  1403.  
  1404. /**
  1405. * @param {String} string
  1406. * @returns {Boolean}
  1407. */
  1408. static isNumber(string) {
  1409. return (/^\d+$/).test(string);
  1410. }
  1411.  
  1412. /**
  1413. * @param {String} id
  1414. * @returns {Promise.<Number>}
  1415. */
  1416. static addFavorite(id) {
  1417. fetch(`https://rule34.xxx/index.php?page=post&s=vote&id=${id}&type=up`);
  1418. return fetch(`https://rule34.xxx/public/addfav.php?id=${id}`)
  1419. .then((response) => {
  1420. return response.text();
  1421. })
  1422. .then((html) => {
  1423. return parseInt(html);
  1424. })
  1425. .catch(() => {
  1426. return Utils.addedFavoriteStatuses.error;
  1427. });
  1428. }
  1429.  
  1430. /**
  1431. * @param {String} id
  1432. */
  1433. static removeFavorite(id) {
  1434. Utils.setIdToBeRemovedOnReload(id);
  1435. fetch(`https://rule34.xxx/index.php?page=favorites&s=delete&id=${id}`);
  1436. }
  1437.  
  1438. /**
  1439. * @param {HTMLInputElement | HTMLTextAreaElement} input
  1440. * @param {String} suggestion
  1441. */
  1442. static insertSuggestion(input, suggestion) {
  1443. const cursorAtEnd = input.selectionStart === input.value.length;
  1444. const firstHalf = input.value.slice(0, input.selectionStart);
  1445. const secondHalf = input.value.slice(input.selectionStart);
  1446. const firstHalfWithPrefixRemoved = firstHalf.replace(/(\s?)(-?)\S+$/, "$1$2");
  1447. const combinedHalves = Utils.removeExtraWhiteSpace(`${firstHalfWithPrefixRemoved}${suggestion} ${secondHalf}`);
  1448. const result = cursorAtEnd ? `${combinedHalves} ` : combinedHalves;
  1449. const selectionStart = firstHalfWithPrefixRemoved.length + suggestion.length + 1;
  1450.  
  1451. input.value = result;
  1452. input.selectionStart = selectionStart;
  1453. input.selectionEnd = selectionStart;
  1454. }
  1455.  
  1456. /**
  1457. * @param {HTMLInputElement | HTMLTextAreaElement} input
  1458. */
  1459. static hideAwesomplete(input) {
  1460. const awesomplete = Utils.getAwesompleteFromInput(input);
  1461.  
  1462. if (awesomplete !== null) {
  1463. awesomplete.querySelector("ul").setAttribute("hidden", "");
  1464. }
  1465. }
  1466.  
  1467. /**
  1468. * @param {String} svg
  1469. * @param {Number} duration
  1470. */
  1471. static showFullscreenIcon(svg, duration = 500) {
  1472. const svgDocument = new DOMParser().parseFromString(svg, "image/svg+xml");
  1473. const svgElement = svgDocument.documentElement;
  1474. const svgOverlay = document.createElement("div");
  1475.  
  1476. svgOverlay.classList.add("fullscreen-icon");
  1477. svgOverlay.innerHTML = new XMLSerializer().serializeToString(svgElement);
  1478. document.body.appendChild(svgOverlay);
  1479. setTimeout(() => {
  1480. svgOverlay.remove();
  1481. }, duration);
  1482. }
  1483.  
  1484. /**
  1485. * @param {String} svg
  1486. * @returns {String}
  1487. */
  1488. static createObjectURLFromSvg(svg) {
  1489. const blob = new Blob([svg], {
  1490. type: "image/svg+xml"
  1491. });
  1492. return URL.createObjectURL(blob);
  1493. }
  1494.  
  1495. /**
  1496. * @param {HTMLElement} element
  1497. * @returns {Boolean}
  1498. */
  1499. static isTypeableInput(element) {
  1500. const tagName = element.tagName.toLowerCase();
  1501.  
  1502. if (tagName === "textarea") {
  1503. return true;
  1504. }
  1505.  
  1506. if (tagName === "input") {
  1507. return Utils.typeableInputs.has(element.getAttribute("type"));
  1508. }
  1509. return false;
  1510. }
  1511.  
  1512. /**
  1513. * @param {KeyboardEvent} event
  1514. * @returns {Boolean}
  1515. */
  1516. static isHotkeyEvent(event) {
  1517. return !event.repeat && !Utils.isTypeableInput(event.target);
  1518. }
  1519.  
  1520. /**
  1521. * @param {Set} a
  1522. * @param {Set} b
  1523. * @returns {Set}
  1524. */
  1525. static union(a, b) {
  1526. const c = new Set(a);
  1527.  
  1528. for (const element of b.values()) {
  1529. c.add(element);
  1530. }
  1531. return c;
  1532. }
  1533.  
  1534. /**
  1535. * @param {Set} a
  1536. * @param {Set} b
  1537. * @returns {Set}
  1538. */
  1539. static difference(a, b) {
  1540. const c = new Set(a);
  1541.  
  1542. for (const element of b.values()) {
  1543. c.delete(element);
  1544. }
  1545. return c;
  1546. }
  1547.  
  1548. static removeUnusedScripts() {
  1549. if (!Utils.onFavoritesPage()) {
  1550. return;
  1551. }
  1552. const scripts = Array.from(document.querySelectorAll("script"));
  1553.  
  1554. for (const script of scripts) {
  1555. if ((/(?:fluidplayer|awesomplete)/).test(script.src || "")) {
  1556. script.remove();
  1557. }
  1558. }
  1559. }
  1560.  
  1561. /**
  1562. * @param {String} tagString
  1563. * @returns {Set.<String>}
  1564. */
  1565. static convertToTagSet(tagString) {
  1566. tagString = Utils.removeExtraWhiteSpace(tagString);
  1567.  
  1568. if (tagString === "") {
  1569. return new Set();
  1570. }
  1571. return new Set(tagString.split(" ").sort());
  1572. }
  1573.  
  1574. /**
  1575. * @param {Set.<String>} tagSet
  1576. * @returns {String}
  1577. */
  1578. static convertToTagString(tagSet) {
  1579. if (tagSet.size === 0) {
  1580. return "";
  1581. }
  1582. return Array.from(tagSet).join(" ");
  1583. }
  1584.  
  1585. /**
  1586. * @returns {String | null}
  1587. */
  1588. static getPostPageId() {
  1589. const match = (/id=(\d+)/).exec(window.location.href);
  1590. return match === null ? null : match[1];
  1591. }
  1592.  
  1593. /**
  1594. * @param {String} searchTag
  1595. * @param {String[]} tags
  1596. * @returns {Boolean}
  1597. */
  1598. static tagsMatchWildcardSearchTag(searchTag, tags) {
  1599. try {
  1600. const wildcardRegex = new RegExp(`^${searchTag.replaceAll(/\*/g, ".*")}$`);
  1601. return tags.some(tag => wildcardRegex.test(tag));
  1602. } catch {
  1603. return false;
  1604. }
  1605. }
  1606.  
  1607. static setupCustomWebComponents() {
  1608. Utils.setupCustomNumberWebComponents();
  1609. }
  1610.  
  1611. static async setupCustomNumberWebComponents() {
  1612. await Utils.sleep(400);
  1613. const numberComponents = Array.from(document.querySelectorAll(".number"));
  1614.  
  1615. for (const element of numberComponents) {
  1616. const numberComponent = new NumberComponent(element);
  1617. }
  1618. }
  1619.  
  1620. /**
  1621. * @param {Number} milliseconds
  1622. * @returns {Number}
  1623. */
  1624. static millisecondsToSeconds(milliseconds) {
  1625. return Utils.roundToTwoDecimalPlaces(milliseconds / 1000);
  1626. }
  1627.  
  1628. /**
  1629. * @returns {Set.<String>}
  1630. */
  1631. static loadCustomTags() {
  1632. return new Set(JSON.parse(localStorage.getItem("customTags")) || []);
  1633. }
  1634.  
  1635. /**
  1636. * @param {String} tags
  1637. */
  1638. static async setCustomTags(tags) {
  1639. for (const tag of Utils.removeExtraWhiteSpace(tags).split(" ")) {
  1640. if (tag === "" || Utils.customTags.has(tag)) {
  1641. continue;
  1642. }
  1643. const isAnOfficialTag = await Utils.isOfficialTag(tag);
  1644.  
  1645. if (!isAnOfficialTag) {
  1646. Utils.customTags.add(tag);
  1647. }
  1648. }
  1649. localStorage.setItem("customTags", JSON.stringify(Array.from(Utils.customTags)));
  1650. }
  1651.  
  1652. /**
  1653. * @returns {String[]}
  1654. */
  1655. static getSavedSearchValues() {
  1656. return Array.from(document.getElementsByClassName("save-search-label"))
  1657. .map(element => element.innerText);
  1658. }
  1659.  
  1660. /**
  1661. * @param {{label: String, value: String, type: String}[]} officialTags
  1662. * @param {String} searchQuery
  1663. * @returns {{label: String, value: String, type: String}[]}
  1664. */
  1665. static addCustomTagsToAutocompleteList(officialTags, searchQuery) {
  1666. const customTags = Array.from(Utils.customTags);
  1667. const officialTagValues = new Set(officialTags.map(officialTag => officialTag.value));
  1668. const mergedTags = officialTags;
  1669.  
  1670. for (const customTag of customTags) {
  1671. if (!officialTagValues.has(customTag) && customTag.startsWith(searchQuery)) {
  1672. mergedTags.unshift({
  1673. label: `${customTag} (custom)`,
  1674. value: customTag,
  1675. type: "custom"
  1676. });
  1677. }
  1678. }
  1679. return mergedTags;
  1680. }
  1681.  
  1682. /**
  1683. * @param {String} searchTag
  1684. * @param {String} savedSearch
  1685. * @returns {Boolean}
  1686. */
  1687. static savedSearchMatchesSearchTag(searchTag, savedSearch) {
  1688. const sanitizedSavedSearch = Utils.removeExtraWhiteSpace(savedSearch.replace(/[~())]/g, ""));
  1689. const savedSearchTagList = sanitizedSavedSearch.split(" ");
  1690.  
  1691. for (const savedSearchTag of savedSearchTagList) {
  1692. if (savedSearchTag.startsWith(searchTag)) {
  1693. return true;
  1694. }
  1695. }
  1696. return false;
  1697. }
  1698.  
  1699. /**
  1700. * @param {String} tag
  1701. * @returns {String}
  1702. */
  1703. static removeStartingHyphen(tag) {
  1704. return tag.replace(/^-/, "");
  1705. }
  1706.  
  1707. /**
  1708. * @param {String} searchTag
  1709. * @returns {{label: String, value: String, type: String}[]}
  1710. */
  1711. static getSavedSearchesForAutocompleteList(searchTag) {
  1712. const minimumSearchTagLength = 3;
  1713.  
  1714. if (searchTag.length < minimumSearchTagLength) {
  1715. return [];
  1716. }
  1717. const maxMatchedSavedSearches = 5;
  1718. const matchedSavedSearches = [];
  1719. let i = 0;
  1720.  
  1721. for (const savedSearch of Utils.getSavedSearchValues()) {
  1722. if (Utils.savedSearchMatchesSearchTag(searchTag, savedSearch)) {
  1723. matchedSavedSearches.push({
  1724. label: `${savedSearch}`,
  1725. value: `${searchTag}_saved_search ${savedSearch}`,
  1726. type: "saved"
  1727. });
  1728. i += 1;
  1729. }
  1730.  
  1731. if (matchedSavedSearches.length > maxMatchedSavedSearches) {
  1732. break;
  1733. }
  1734. }
  1735. return matchedSavedSearches;
  1736. }
  1737.  
  1738. static removeSavedSearchPrefix(suggestion) {
  1739. return suggestion.replace(/^\S+_saved_search /, "");
  1740. }
  1741.  
  1742. /**
  1743. * @param {Boolean} value
  1744. */
  1745. static toggleThumbHoverOutlines(value) {
  1746. // insertStyleHTML(value ? STYLES.thumbHoverOutlineDisabled : STYLES.thumbHoverOutline, "thumb-hover-outlines");
  1747. }
  1748.  
  1749. /**
  1750. * @param {Number} timestamp
  1751. * @returns {String}
  1752. */
  1753. static convertTimestampToDate(timestamp) {
  1754. const date = new Date(timestamp);
  1755. const day = date.getDate();
  1756. const month = date.getMonth() + 1;
  1757. const year = date.getFullYear();
  1758. return `${year}-${month}-${day}`;
  1759. }
  1760.  
  1761. /**
  1762. * @returns {String}
  1763. */
  1764. static getSortingMethod() {
  1765. const sortingMethodSelect = document.getElementById("sorting-method");
  1766. return sortingMethodSelect === null ? "default" : sortingMethodSelect.value;
  1767. }
  1768.  
  1769. /**
  1770. * @returns {HTMLDivElement}
  1771. */
  1772. static createFavoritesSearchGalleryContainer() {
  1773. const container = document.createElement("div");
  1774.  
  1775. container.id = "favorites-search-gallery";
  1776. document.body.appendChild(container);
  1777. return container;
  1778. }
  1779.  
  1780. /**
  1781. * @param {HTMLElement} element
  1782. * @param {InsertPosition} position
  1783. * @param {String} html
  1784. */
  1785. static insertHTMLAndExtractStyle(element, position, html) {
  1786. const dom = new DOMParser().parseFromString(html, "text/html");
  1787. const styles = Array.from(dom.querySelectorAll("style"));
  1788.  
  1789. for (const style of styles) {
  1790. Utils.insertStyleHTML(style.innerHTML);
  1791. style.remove();
  1792. }
  1793. element.insertAdjacentHTML(position, dom.body.innerHTML);
  1794. }
  1795.  
  1796. /**
  1797. * @param {InsertPosition} position
  1798. * @param {String} html
  1799. */
  1800. static insertFavoritesSearchGalleryHTML(position, html) {
  1801. Utils.insertHTMLAndExtractStyle(Utils.favoritesSearchGalleryContainer, position, html);
  1802. }
  1803.  
  1804. /**
  1805. * @param {String} str
  1806. * @returns {String}
  1807. */
  1808. static removeNonNumericCharacters(str) {
  1809. return str.replaceAll(/\D/g, "");
  1810. }
  1811.  
  1812. /**
  1813. * @param {HTMLElement} thumb
  1814. * @returns {String}
  1815. */
  1816. static getIdFromThumb(thumb) {
  1817. const id = thumb.getAttribute("id");
  1818.  
  1819. if (id !== null) {
  1820. return Utils.removeNonNumericCharacters(id);
  1821. }
  1822. const anchor = thumb.querySelector("a");
  1823.  
  1824. if (anchor !== null && anchor.hasAttribute("id")) {
  1825. return Utils.removeNonNumericCharacters(anchor.id);
  1826. }
  1827.  
  1828. if (anchor !== null && anchor.hasAttribute("href")) {
  1829. const match = (/id=(\d+)$/).exec(anchor.href);
  1830.  
  1831. if (match !== null) {
  1832. return match[1];
  1833. }
  1834. }
  1835. const image = thumb.querySelector("img");
  1836. const match = (/\?(\d+)$/).exec(image.src);
  1837. return match[1];
  1838. }
  1839.  
  1840. static deletePersistentData() {
  1841. const desktopSuffix = Utils.onMobileDevice() ? "" : " Tag modifications and saved searches will be preserved.";
  1842.  
  1843. const message = `Are you sure you want to reset? This will delete all cached favorites, and preferences.${desktopSuffix}`;
  1844.  
  1845. if (confirm(message)) {
  1846. const persistentLocalStorageKeys = new Set(["customTags", "savedSearches"]);
  1847.  
  1848. Object.keys(localStorage).forEach((key) => {
  1849. if (!persistentLocalStorageKeys.has(key)) {
  1850. localStorage.removeItem(key);
  1851. }
  1852. });
  1853. indexedDB.deleteDatabase(FavoritesDatabaseWrapper.databaseName);
  1854. }
  1855. }
  1856.  
  1857. /**
  1858. * @param {String} id
  1859. * @returns {String}
  1860. */
  1861. static getPostPageURL(id) {
  1862. return `https://rule34.xxx/index.php?page=post&s=view&id=${id}`;
  1863. }
  1864.  
  1865. /**
  1866. * @param {String} id
  1867. */
  1868. static openPostInNewTab(id) {
  1869. window.open(Utils.getPostPageURL(id), "_blank");
  1870. }
  1871.  
  1872. /**
  1873. * @param {Function} initializer
  1874. */
  1875. static addStaticInitializer(initializer) {
  1876. Utils.staticInitializers.push(initializer);
  1877. }
  1878.  
  1879. static invokeStaticInitializers() {
  1880. for (const initializer of Utils.staticInitializers) {
  1881. initializer();
  1882. }
  1883. Utils.staticInitializers = null;
  1884. }
  1885.  
  1886. /**
  1887. * @returns {Number}
  1888. */
  1889. static loadAllowedRatings() {
  1890. return parseInt(Utils.getPreference("allowedRatings", 7));
  1891. }
  1892.  
  1893. /**
  1894. * @param {Set} a
  1895. * @param {Set} b
  1896. * @returns {Set}
  1897. */
  1898. static symmetricDifference(a, b) {
  1899. return Utils.union(Utils.difference(a, b), Utils.difference(b, a));
  1900. }
  1901.  
  1902. static clearOriginalFavoritesPage() {
  1903. const thumbs = Array.from(document.getElementsByClassName("thumb"));
  1904. let content = document.getElementById("content");
  1905.  
  1906. if (content === null && thumbs.length > 0) {
  1907. content = thumbs[0].closest("body>div");
  1908. }
  1909.  
  1910. if (content !== null) {
  1911. content.remove();
  1912. }
  1913. setTimeout(() => {
  1914. dispatchEvent(new CustomEvent("originalFavoritesCleared", {
  1915. detail: thumbs
  1916. }));
  1917. }, 1000);
  1918. }
  1919.  
  1920. /**
  1921. * @param {String} id
  1922. * @returns {String}
  1923. */
  1924. static getPostAPIURL(id) {
  1925. return `https://api.rule34.xxx//index.php?page=dapi&s=post&q=index&id=${id}`;
  1926. }
  1927.  
  1928. /**
  1929. * @returns {Promise<String>}
  1930. */
  1931. static getImageExtensionFromThumb(thumb) {
  1932. if (Utils.isVideo(thumb)) {
  1933. return "mp4";
  1934. }
  1935.  
  1936. if (Utils.isGif(thumb)) {
  1937. return "gif";
  1938. }
  1939.  
  1940. if (Utils.extensionIsKnown(thumb.id)) {
  1941. return Utils.getImageExtension(thumb.id);
  1942. }
  1943. return Utils.fetchImageExtension(thumb);
  1944. }
  1945.  
  1946. /**
  1947. * @param {HTMLElement} thumb
  1948. * @returns {Promise<String>}
  1949. */
  1950. static fetchImageExtension(thumb) {
  1951. return fetch(Utils.getPostAPIURL(thumb.id))
  1952. .then((response) => {
  1953. return response.text();
  1954. })
  1955. .then((html) => {
  1956. const dom = new DOMParser().parseFromString(html, "text/html");
  1957. const metadata = dom.querySelector("post");
  1958. const extension = Utils.getExtensionFromImageURL(metadata.getAttribute("file_url"));
  1959.  
  1960. Utils.assignImageExtension(thumb.id, extension);
  1961. return extension;
  1962. })
  1963. .catch((error) => {
  1964. console.error(error);
  1965. return "jpg";
  1966. });
  1967. }
  1968.  
  1969. /**
  1970. * @param {HTMLElement} thumb
  1971. * @returns {Promise<String>}
  1972. */
  1973. static async getOriginalImageURLWithExtension(thumb) {
  1974. const extension = await Utils.getImageExtensionFromThumb(thumb);
  1975. return Utils.getOriginalImageURL(thumb.querySelector("img").src).replace(".jpg", `.${extension}`);
  1976. }
  1977.  
  1978. /**
  1979. * @param {HTMLElement} thumb
  1980. */
  1981. static async openOriginalImageInNewTab(thumb) {
  1982. try {
  1983. const imageURL = await Utils.getOriginalImageURLWithExtension(thumb);
  1984.  
  1985. window.open(imageURL);
  1986. } catch (error) {
  1987. console.error(error);
  1988. }
  1989. }
  1990.  
  1991. /**
  1992. * @returns {String}
  1993. */
  1994. static getSearchPageAPIURL() {
  1995. const postsPerPage = 42;
  1996. const apiURL = `https://api.rule34.xxx/index.php?page=dapi&s=post&q=index&limit=${postsPerPage}`;
  1997. let blacklistedTags = ` ${Utils.negateTags(Utils.tagBlacklist)}`.replace(/\s-/g, "+-");
  1998. let pageNumber = (/&pid=(\d+)/).exec(location.href);
  1999. let searchTags = (/&tags=([^&]*)/).exec(location.href);
  2000.  
  2001. pageNumber = pageNumber === null ? 0 : Math.floor(parseInt(pageNumber[1]) / postsPerPage);
  2002. searchTags = searchTags === null ? "" : searchTags[1];
  2003.  
  2004. if (searchTags === "all") {
  2005. searchTags = "";
  2006. blacklistedTags = "";
  2007. }
  2008. return `${apiURL}&tags=${searchTags}${blacklistedTags}&pid=${pageNumber}`;
  2009. }
  2010.  
  2011. static findImageExtensionsOnSearchPage() {
  2012. const searchPageAPIURL = Utils.getSearchPageAPIURL();
  2013. return fetch(searchPageAPIURL)
  2014. .then((response) => {
  2015. if (response.ok) {
  2016. return response.text();
  2017. }
  2018. return null;
  2019. })
  2020. .then((html) => {
  2021. if (html === null) {
  2022. console.error(`Failed to fetch: ${searchPageAPIURL}`);
  2023. }
  2024. const dom = new DOMParser().parseFromString(`<div>${html}</div>`, "text/html");
  2025. const posts = Array.from(dom.getElementsByTagName("post"));
  2026.  
  2027. for (const post of posts) {
  2028. const tags = post.getAttribute("tags");
  2029. const id = post.getAttribute("id");
  2030. const originalImageURL = post.getAttribute("file_url");
  2031. const tagSet = Utils.convertToTagSet(tags);
  2032. const thumb = document.getElementById(id);
  2033.  
  2034. if (!tagSet.has("video") && originalImageURL.endsWith("mp4") && thumb !== null) {
  2035. const image = Utils.getImageFromThumb(thumb);
  2036.  
  2037. image.setAttribute("tags", `${image.getAttribute("tags")} video`);
  2038. Utils.setContentType(image, "video");
  2039. } else if (!tagSet.has("gif") && originalImageURL.endsWith("gif") && thumb !== null) {
  2040. const image = Utils.getImageFromThumb(thumb);
  2041.  
  2042. image.setAttribute("tags", `${image.getAttribute("tags")} gif`);
  2043. Utils.setContentType(image, "gif");
  2044. }
  2045. const isAnImage = Utils.getContentType(tags) === "image";
  2046. const isBlacklisted = originalImageURL === "https://api-cdn.rule34.xxx/images//";
  2047.  
  2048. if (!isAnImage || isBlacklisted) {
  2049. continue;
  2050. }
  2051. const extension = Utils.getExtensionFromImageURL(originalImageURL);
  2052.  
  2053. Utils.assignImageExtension(id, extension);
  2054. }
  2055. })
  2056. .catch((error) => {
  2057. console.error(error);
  2058. });
  2059. }
  2060.  
  2061. static async setupOriginalImageLinksOnSearchPage() {
  2062. if (!Utils.onSearchPage()) {
  2063. return;
  2064. }
  2065.  
  2066. if (Gallery.disabled) {
  2067. await Utils.findImageExtensionsOnSearchPage();
  2068. Utils.setupOriginalImageLinksOnSearchPageHelper();
  2069. } else {
  2070. window.addEventListener("foundExtensionsOnSearchPage", () => {
  2071. Utils.setupOriginalImageLinksOnSearchPageHelper();
  2072. }, {
  2073. once: true
  2074. });
  2075. }
  2076. }
  2077.  
  2078. static async setupOriginalImageLinksOnSearchPageHelper() {
  2079. try {
  2080. for (const thumb of Utils.getAllThumbs()) {
  2081. await Utils.setupOriginalImageLinkOnSearchPage(thumb);
  2082. }
  2083. } catch (error) {
  2084. console.error(error);
  2085. }
  2086. }
  2087.  
  2088. /**
  2089. * @param {HTMLElement} thumb
  2090. */
  2091. static async setupOriginalImageLinkOnSearchPage(thumb) {
  2092. const anchor = thumb.querySelector("a");
  2093. const imageURL = await Utils.getOriginalImageURLWithExtension(thumb);
  2094. const thumbURL = anchor.href;
  2095.  
  2096. anchor.href = imageURL;
  2097. anchor.onclick = (event) => {
  2098. if (!event.ctrlKey) {
  2099. event.preventDefault();
  2100. }
  2101. };
  2102. anchor.onmousedown = (event) => {
  2103. if (event.ctrlKey) {
  2104. return;
  2105. }
  2106. event.preventDefault();
  2107. const middleClick = event.button === Utils.clickCodes.middle;
  2108. const leftClick = event.button === Utils.clickCodes.left;
  2109. const shiftClick = leftClick && event.shiftKey;
  2110.  
  2111. if (leftClick && Gallery.disabled) {
  2112. document.location = thumbURL;
  2113. } else if (middleClick || shiftClick) {
  2114. window.open(thumbURL);
  2115. }
  2116. };
  2117. }
  2118.  
  2119. static prepareSearchPage() {
  2120. if (!Utils.onSearchPage()) {
  2121. return;
  2122. }
  2123.  
  2124. for (const thumb of Utils.getAllThumbs()) {
  2125. Utils.removeTitleFromImage(Utils.getImageFromThumb(thumb));
  2126. Utils.assignContentType(thumb);
  2127. thumb.id = Utils.removeNonNumericCharacters(Utils.getIdFromThumb(thumb));
  2128. }
  2129. }
  2130.  
  2131. /**
  2132. * @returns {Object.<String, Number>}
  2133. */
  2134. static loadDiscoveredImageExtensions() {
  2135. return JSON.parse(localStorage.getItem(Utils.localStorageKeys.imageExtensions)) || {};
  2136. }
  2137.  
  2138. /**
  2139. * @param {String | Number} id
  2140. * @returns {String}
  2141. */
  2142. static getImageExtension(id) {
  2143. return Utils.extensionDecodings[Utils.imageExtensions[parseInt(id)]];
  2144. }
  2145.  
  2146. /**
  2147. * @param {String | Number} id
  2148. * @param {String} extension
  2149. */
  2150. static setImageExtension(id, extension) {
  2151. Utils.imageExtensions[parseInt(id)] = Utils.extensionEncodings[extension];
  2152. }
  2153.  
  2154. /**
  2155. * @param {String} id
  2156. * @returns {Boolean}
  2157. */
  2158. static extensionIsKnown(id) {
  2159. return Utils.getImageExtension(id) !== undefined;
  2160. }
  2161.  
  2162. static updateStoredImageExtensions() {
  2163. Utils.recentlyDiscoveredImageExtensionCount += 1;
  2164.  
  2165. if (Utils.recentlyDiscoveredImageExtensionCount >= Utils.settings.extensionsFoundBeforeSavingCount) {
  2166. this.storeAllImageExtensions();
  2167. }
  2168. }
  2169.  
  2170. static storeAllImageExtensions() {
  2171. if (!Utils.onFavoritesPage()) {
  2172. return;
  2173. }
  2174. Utils.recentlyDiscoveredImageExtensionCount = 0;
  2175. localStorage.setItem(Utils.localStorageKeys.imageExtensions, JSON.stringify(Utils.imageExtensions));
  2176. }
  2177.  
  2178. static isAnAnimatedExtension(extension) {
  2179. return extension === "mp4" || extension === "gif";
  2180. }
  2181.  
  2182. /**
  2183. * @param {String} id
  2184. * @param {String} extension
  2185. */
  2186. static assignImageExtension(id, extension) {
  2187. if (Utils.extensionIsKnown(id) || Utils.isAnAnimatedExtension(extension)) {
  2188. return;
  2189. }
  2190. Utils.imageExtensionAssignmentCooldown.restart();
  2191. Utils.setImageExtension(id, extension);
  2192. Utils.updateStoredImageExtensions();
  2193. }
  2194.  
  2195. static initializeImageExtensionAssignmentCooldown() {
  2196. Utils.imageExtensionAssignmentCooldown = new Cooldown(1000);
  2197. Utils.imageExtensionAssignmentCooldown.onCooldownEnd = () => {
  2198. if (Utils.recentlyDiscoveredImageExtensionCount > 0) {
  2199. Utils.storeAllImageExtensions();
  2200. }
  2201. };
  2202. }
  2203.  
  2204. /**
  2205. * @returns {Boolean}
  2206. */
  2207. static usingIOS() {
  2208. return (/iPhone|iPad|iPod/).test(navigator.userAgent);
  2209. }
  2210. }
  2211.  
  2212. class HoldButton extends HTMLElement {
  2213. static {
  2214. Utils.addStaticInitializer(() => {
  2215. customElements.define("hold-button", HoldButton);
  2216. });
  2217. }
  2218.  
  2219. /**
  2220. * @type {Number}
  2221. */
  2222. static defaultPollingTime = 100;
  2223. /**
  2224. * @type {Number}
  2225. */
  2226. static minPollingTime = 40;
  2227. /**
  2228. * @type {Number}
  2229. */
  2230. static maxPollingTime = 500;
  2231.  
  2232. /**
  2233. * @type {Number}
  2234. */
  2235. intervalId;
  2236. /**
  2237. * @type {Number}
  2238. */
  2239. timeoutId;
  2240. /**
  2241. * @type {Number}
  2242. */
  2243. pollingTime = HoldButton.defaultPollingTime;
  2244. /**
  2245. * @type {Boolean}
  2246. */
  2247. holdingDown = false;
  2248.  
  2249. connectedCallback() {
  2250. if (Utils.onMobileDevice()) {
  2251. return;
  2252. }
  2253. this.addEventListeners();
  2254. this.setPollingTime(this.getAttribute("pollingtime"));
  2255. }
  2256.  
  2257. attributeChangedCallback(name, oldValue, newValue) {
  2258. switch (name) {
  2259. case "pollingtime":
  2260. this.setPollingTime(newValue);
  2261. break;
  2262.  
  2263. default:
  2264. break;
  2265. }
  2266. }
  2267.  
  2268. /**
  2269. * @param {String} newValue
  2270. */
  2271. setPollingTime(newValue) {
  2272. this.stopPolling();
  2273. const pollingTime = parseFloat(newValue) || HoldButton.defaultPollingTime;
  2274.  
  2275. this.pollingTime = Utils.clamp(Math.round(pollingTime), HoldButton.minPollingTime, HoldButton.maxPollingTime);
  2276. }
  2277.  
  2278. addEventListeners() {
  2279. this.addEventListener("mousedown", (event) => {
  2280. if (event.button === 0) {
  2281. this.holdingDown = true;
  2282. this.startPolling();
  2283. }
  2284. }, {
  2285. passive: true
  2286. });
  2287.  
  2288. this.addEventListener("mouseup", (event) => {
  2289. if (event.button === 0) {
  2290. this.holdingDown = false;
  2291. this.stopPolling();
  2292. }
  2293. }, {
  2294. passive: true
  2295. });
  2296.  
  2297. this.addEventListener("mouseleave", () => {
  2298. if (this.holdingDown) {
  2299. this.onMouseLeaveWhileHoldingDown();
  2300. this.holdingDown = false;
  2301. }
  2302. this.stopPolling();
  2303. }, {
  2304. passive: true
  2305. });
  2306. }
  2307.  
  2308. startPolling() {
  2309. this.timeoutId = setTimeout(() => {
  2310. this.intervalId = setInterval(() => {
  2311. this.onmousehold();
  2312. }, this.pollingTime);
  2313. }, this.pollingTime);
  2314. }
  2315.  
  2316. stopPolling() {
  2317. clearTimeout(this.timeoutId);
  2318. clearInterval(this.intervalId);
  2319. }
  2320.  
  2321. onmousehold() {
  2322. }
  2323.  
  2324. onMouseLeaveWhileHoldingDown() {
  2325. }
  2326. }
  2327.  
  2328. class NumberComponent {
  2329. /**
  2330. * @type {HTMLInputElement}
  2331. */
  2332. input;
  2333. /**
  2334. * @type {HoldButton}
  2335. */
  2336. upArrow;
  2337. /**
  2338. * @type {HoldButton}
  2339. */
  2340. downArrow;
  2341. /**
  2342. * @type {Number}
  2343. */
  2344. increment;
  2345.  
  2346. /**
  2347. * @type {Boolean}
  2348. */
  2349. get allSubComponentsConnected() {
  2350. return this.input !== null && this.upArrow !== null && this.downArrow !== null;
  2351. }
  2352.  
  2353. /**
  2354. * @param {HTMLDivElement} element
  2355. */
  2356. constructor(element) {
  2357. this.connectSubElements(element);
  2358. this.initializeFields();
  2359. this.addEventListeners();
  2360. }
  2361.  
  2362. initializeFields() {
  2363. if (!this.allSubComponentsConnected) {
  2364. return;
  2365. }
  2366. this.increment = Utils.roundToTwoDecimalPlaces(parseFloat(this.input.getAttribute("step")) || 1);
  2367.  
  2368. if (this.input.onchange === null) {
  2369. this.input.onchange = () => { };
  2370. }
  2371. }
  2372.  
  2373. /**
  2374. * @param {HTMLDivElement} element
  2375. */
  2376. connectSubElements(element) {
  2377. this.input = element.querySelector("input");
  2378. this.upArrow = element.querySelector(".number-arrow-up");
  2379. this.downArrow = element.querySelector(".number-arrow-down");
  2380. }
  2381.  
  2382. addEventListeners() {
  2383. if (!this.allSubComponentsConnected) {
  2384. return;
  2385. }
  2386. this.upArrow.onmousehold = () => {
  2387. this.incrementInput(true);
  2388. };
  2389. this.downArrow.onmousehold = () => {
  2390. this.incrementInput(false);
  2391. };
  2392. this.upArrow.addEventListener("mousedown", (event) => {
  2393. if (event.button === 0) {
  2394. this.incrementInput(true);
  2395. }
  2396. }, {
  2397. passive: true
  2398. });
  2399. this.downArrow.addEventListener("mousedown", (event) => {
  2400. if (event.button === 0) {
  2401. this.incrementInput(false);
  2402. }
  2403. }, {
  2404. passive: true
  2405. });
  2406. this.upArrow.addEventListener("mouseup", () => {
  2407. this.input.onchange();
  2408. }, {
  2409. passive: true
  2410. });
  2411. this.downArrow.addEventListener("mouseup", () => {
  2412. this.input.onchange();
  2413. }, {
  2414. passive: true
  2415. });
  2416. this.upArrow.onMouseLeaveWhileHoldingDown = () => {
  2417. this.input.onchange();
  2418. };
  2419. this.downArrow.onMouseLeaveWhileHoldingDown = () => {
  2420. this.input.onchange();
  2421. };
  2422. }
  2423.  
  2424. /**
  2425. * @param {Boolean} add
  2426. */
  2427. incrementInput(add) {
  2428. const currentValue = parseFloat(this.input.value) || 1;
  2429. const incrementedValue = add ? currentValue + this.increment : currentValue - this.increment;
  2430.  
  2431. this.input.value = Utils.clamp(incrementedValue, 0, 9999);
  2432. }
  2433. }
  2434.  
  2435. class Cooldown {
  2436. /**
  2437. * @type {Number}
  2438. */
  2439. timeout;
  2440. /**
  2441. * @type {Number}
  2442. */
  2443. waitTime;
  2444. /**
  2445. * @type {Boolean}
  2446. */
  2447. skipCooldown;
  2448. /**
  2449. * @type {Boolean}
  2450. */
  2451. debounce;
  2452. /**
  2453. * @type {Boolean}
  2454. */
  2455. debouncing;
  2456. /**
  2457. * @type {Function}
  2458. */
  2459. onDebounceEnd;
  2460. /**
  2461. * @type {Function}
  2462. */
  2463. onCooldownEnd;
  2464.  
  2465. get ready() {
  2466. if (this.skipCooldown) {
  2467. return true;
  2468. }
  2469.  
  2470. if (this.timeout === null) {
  2471. this.start();
  2472. return true;
  2473. }
  2474.  
  2475. if (this.debounce) {
  2476. this.startDebounce();
  2477. }
  2478. return false;
  2479. }
  2480.  
  2481. /**
  2482. * @param {Number} waitTime
  2483. * @param {Boolean} debounce
  2484. */
  2485. constructor(waitTime, debounce = false) {
  2486. this.timeout = null;
  2487. this.waitTime = waitTime;
  2488. this.skipCooldown = false;
  2489. this.debounce = debounce;
  2490. this.debouncing = false;
  2491. this.onDebounceEnd = () => { };
  2492. this.onCooldownEnd = () => { };
  2493. }
  2494.  
  2495. startDebounce() {
  2496. this.debouncing = true;
  2497. clearTimeout(this.timeout);
  2498. this.start();
  2499. }
  2500.  
  2501. start() {
  2502. this.timeout = setTimeout(() => {
  2503. this.timeout = null;
  2504.  
  2505. if (this.debouncing) {
  2506. this.onDebounceEnd();
  2507. this.debouncing = false;
  2508. }
  2509. this.onCooldownEnd();
  2510. }, this.waitTime);
  2511. }
  2512.  
  2513. stop() {
  2514. if (this.timeout === null) {
  2515. return;
  2516. }
  2517. clearTimeout(this.timeout);
  2518. this.timeout = null;
  2519. }
  2520.  
  2521. restart() {
  2522. this.stop();
  2523. this.start();
  2524. }
  2525. }
  2526.  
  2527. class MetadataSearchExpression {
  2528. /**
  2529. * @type {String}
  2530. */
  2531. metric;
  2532. /**
  2533. * @type {String}
  2534. */
  2535. operator;
  2536. /**
  2537. * @type {String | Number}
  2538. */
  2539. value;
  2540.  
  2541. /**
  2542. * @param {String} metric
  2543. * @param {String} operator
  2544. * @param {String} value
  2545. */
  2546. constructor(metric, operator, value) {
  2547. this.metric = metric;
  2548. this.operator = operator;
  2549. this.value = this.setValue(value);
  2550. }
  2551.  
  2552. /**
  2553. * @param {String} value
  2554. * @returns {String | Number}
  2555. */
  2556. setValue(value) {
  2557. if (!Utils.isNumber(value)) {
  2558. return value;
  2559. }
  2560.  
  2561. if (this.metric === "id" && this.operator === ":") {
  2562. return value;
  2563. }
  2564. return parseInt(value);
  2565. }
  2566. }
  2567.  
  2568. class PostMetadata {
  2569. /**
  2570. * @type {Map.<String, PostMetadata>}
  2571. */
  2572. static allMetadata = new Map();
  2573. static parser = new DOMParser();
  2574. /**
  2575. * @type {PostMetadata[]}
  2576. */
  2577. static missingMetadataFetchQueue = [];
  2578. /**
  2579. * @type {PostMetadata[]}
  2580. */
  2581. static deletedPostFetchQueue = [];
  2582. static currentlyFetchingFromQueue = false;
  2583. static allFavoritesLoaded = false;
  2584. static fetchDelay = {
  2585. normal: 10,
  2586. deleted: 300
  2587. };
  2588. static postStatisticsRegex = /Posted:\s*(\S+\s\S+).*Size:\s*(\d+)x(\d+).*Rating:\s*(\S+).*Score:\s*(\d+)/gm;
  2589. static settings = {
  2590. verifyTags: true
  2591. };
  2592. /**
  2593. * @param {PostMetadata} missingMetadata
  2594. */
  2595. static async fetchMissingMetadata(missingMetadata) {
  2596. if (missingMetadata !== undefined) {
  2597. PostMetadata.missingMetadataFetchQueue.push(missingMetadata);
  2598. }
  2599.  
  2600. if (PostMetadata.currentlyFetchingFromQueue) {
  2601. return;
  2602. }
  2603. PostMetadata.currentlyFetchingFromQueue = true;
  2604.  
  2605. while (PostMetadata.missingMetadataFetchQueue.length > 0) {
  2606. const metadata = this.missingMetadataFetchQueue.pop();
  2607.  
  2608. if (metadata.postIsDeleted) {
  2609. metadata.populateMetadataFromPost();
  2610. } else {
  2611. metadata.populateMetadataFromAPI(true);
  2612. }
  2613. await Utils.sleep(metadata.fetchDelay);
  2614. }
  2615. PostMetadata.currentlyFetchingFromQueue = false;
  2616. }
  2617.  
  2618. /**
  2619. * @param {String} rating
  2620. * @returns {Number}
  2621. */
  2622. static encodeRating(rating) {
  2623. return {
  2624. "Explicit": 4,
  2625. "E": 4,
  2626. "e": 4,
  2627. "Questionable": 2,
  2628. "Q": 2,
  2629. "q": 2,
  2630. "Safe": 1,
  2631. "S": 1,
  2632. "s": 1
  2633. }[rating] || 4;
  2634. }
  2635.  
  2636. static {
  2637. Utils.addStaticInitializer(() => {
  2638. if (Utils.onFavoritesPage()) {
  2639. window.addEventListener("favoritesLoaded", () => {
  2640. PostMetadata.allFavoritesLoaded = true;
  2641. PostMetadata.missingMetadataFetchQueue = PostMetadata.missingMetadataFetchQueue.concat(PostMetadata.deletedPostFetchQueue);
  2642. PostMetadata.fetchMissingMetadata();
  2643. }, {
  2644. once: true
  2645. });
  2646. }
  2647. });
  2648. }
  2649. /**
  2650. * @type {String}
  2651. */
  2652. id;
  2653. /**
  2654. * @type {Number}
  2655. */
  2656. width;
  2657. /**
  2658. * @type {Number}
  2659. */
  2660. height;
  2661. /**
  2662. * @type {Number}
  2663. */
  2664. score;
  2665. /**
  2666. * @type {Number}
  2667. */
  2668. rating;
  2669. /**
  2670. * @type {Number}
  2671. */
  2672. creationTimestamp;
  2673. /**
  2674. * @type {Number}
  2675. */
  2676. lastChangedTimestamp;
  2677. /**
  2678. * @type {Boolean}
  2679. */
  2680. postIsDeleted;
  2681.  
  2682. /**
  2683. * @type {String}
  2684. */
  2685. get apiURL() {
  2686. return Utils.getPostAPIURL(this.id);
  2687. }
  2688.  
  2689. /**
  2690. * @type {String}
  2691. */
  2692. get postURL() {
  2693. return Utils.getPostPageURL(this.id);
  2694. }
  2695.  
  2696. /**
  2697. * @type {Number}
  2698. */
  2699. get fetchDelay() {
  2700. return this.postIsDeleted ? PostMetadata.fetchDelay.deleted : PostMetadata.fetchDelay.normal;
  2701. }
  2702.  
  2703. /**
  2704. * @type {Boolean}
  2705. */
  2706. get isEmpty() {
  2707. return this.width === 0 && this.height === 0;
  2708. }
  2709.  
  2710. /**
  2711. * @type {String}
  2712. */
  2713. get json() {
  2714. return JSON.stringify({
  2715. width: this.width,
  2716. height: this.height,
  2717. score: this.score,
  2718. rating: this.rating,
  2719. create: this.creationTimestamp,
  2720. change: this.lastChangedTimestamp,
  2721. deleted: this.postIsDeleted
  2722. });
  2723. }
  2724.  
  2725. /**
  2726. * @type {Number}
  2727. */
  2728. get pixelCount() {
  2729. return this.width * this.height;
  2730. }
  2731.  
  2732. /**
  2733. * @param {String} id
  2734. * @param {Object.<String, String>} record
  2735. */
  2736. constructor(id, record) {
  2737. this.id = id;
  2738. this.setDefaults();
  2739. this.populateMetadata(record);
  2740. this.addInstanceToAllMetadata();
  2741. }
  2742.  
  2743. setDefaults() {
  2744. this.width = 0;
  2745. this.height = 0;
  2746. this.score = 0;
  2747. this.creationTimestamp = 0;
  2748. this.lastChangedTimestamp = 0;
  2749. this.rating = 4;
  2750. this.postIsDeleted = false;
  2751. }
  2752.  
  2753. /**
  2754. * @param {Object.<String, String>} record
  2755. */
  2756. populateMetadata(record) {
  2757. if (record === undefined) {
  2758. this.populateMetadataFromAPI();
  2759. } else if (record === null) {
  2760. PostMetadata.fetchMissingMetadata(this, true);
  2761. } else {
  2762. this.populateMetadataFromRecord(JSON.parse(record));
  2763.  
  2764. if (this.isEmpty) {
  2765. PostMetadata.fetchMissingMetadata(this, true);
  2766. }
  2767. }
  2768. }
  2769.  
  2770. /**
  2771. * @param {Boolean} missingInDatabase
  2772. */
  2773. populateMetadataFromAPI(missingInDatabase = false) {
  2774. fetch(this.apiURL)
  2775. .then((response) => {
  2776. return response.text();
  2777. })
  2778. .then((html) => {
  2779. const dom = PostMetadata.parser.parseFromString(html, "text/html");
  2780. const metadata = dom.querySelector("post");
  2781.  
  2782. if (metadata === null) {
  2783. throw new Error(`metadata is null - ${this.apiURL}`, {
  2784. cause: "DeletedMetadata"
  2785. });
  2786. }
  2787. this.width = parseInt(metadata.getAttribute("width"));
  2788. this.height = parseInt(metadata.getAttribute("height"));
  2789. this.score = parseInt(metadata.getAttribute("score"));
  2790. this.rating = PostMetadata.encodeRating(metadata.getAttribute("rating"));
  2791. this.creationTimestamp = Date.parse(metadata.getAttribute("created_at"));
  2792. this.lastChangedTimestamp = parseInt(metadata.getAttribute("change"));
  2793.  
  2794. if (PostMetadata.settings.verifyTags) {
  2795. Post.verifyTags(this.id, metadata.getAttribute("tags"), metadata.getAttribute("file_url"));
  2796. }
  2797. const extension = Utils.getExtensionFromImageURL(metadata.getAttribute("file_url"));
  2798.  
  2799. if (extension !== "mp4") {
  2800. Utils.assignImageExtension(this.id, extension);
  2801. }
  2802.  
  2803. if (missingInDatabase) {
  2804. dispatchEvent(new CustomEvent("missingMetadata", {
  2805. detail: this.id
  2806. }));
  2807. }
  2808. })
  2809. .catch((error) => {
  2810. if (error.cause === "DeletedMetadata") {
  2811. this.postIsDeleted = true;
  2812. PostMetadata.deletedPostFetchQueue.push(this);
  2813. } else if (error.message === "Failed to fetch") {
  2814. PostMetadata.missingMetadataFetchQueue.push(this);
  2815. } else {
  2816. console.error(error);
  2817. }
  2818. });
  2819. }
  2820.  
  2821. /**
  2822. * @param {Object.<String, String>} record
  2823. */
  2824. populateMetadataFromRecord(record) {
  2825. this.width = record.width;
  2826. this.height = record.height;
  2827. this.score = record.score;
  2828. this.rating = record.rating;
  2829. this.creationTimestamp = record.create;
  2830. this.lastChangedTimestamp = record.change;
  2831. this.postIsDeleted = record.deleted;
  2832. }
  2833.  
  2834. populateMetadataFromPost() {
  2835. fetch(this.postURL)
  2836. .then((response) => {
  2837. return response.text();
  2838. })
  2839. .then((html) => {
  2840. const dom = PostMetadata.parser.parseFromString(html, "text/html");
  2841. const statistics = dom.getElementById("stats");
  2842.  
  2843. if (statistics === null) {
  2844. return;
  2845. }
  2846. const textContent = Utils.replaceLineBreaks(statistics.textContent.trim(), " ");
  2847. const match = PostMetadata.postStatisticsRegex.exec(textContent);
  2848.  
  2849. PostMetadata.postStatisticsRegex.lastIndex = 0;
  2850.  
  2851. if (!match) {
  2852. return;
  2853. }
  2854. this.width = parseInt(match[2]);
  2855. this.height = parseInt(match[3]);
  2856. this.score = parseInt(match[5]);
  2857. this.rating = PostMetadata.encodeRating(match[4]);
  2858. this.creationTimestamp = Date.parse(match[1]);
  2859. this.lastChangedTimestamp = this.creationTimestamp / 1000;
  2860.  
  2861. if (PostMetadata.allFavoritesLoaded) {
  2862. dispatchEvent(new CustomEvent("missingMetadata", {
  2863. detail: this.id
  2864. }));
  2865. }
  2866. })
  2867. .catch((error) => {
  2868. console.error(error);
  2869. });
  2870. }
  2871.  
  2872. /**
  2873. * @param {{metric: String, operator: String, value: String, negated: Boolean}[]} filters
  2874. * @returns {Boolean}
  2875. */
  2876. satisfiesAllFilters(filters) {
  2877. for (const expression of filters) {
  2878. if (!this.satisfiesExpression(expression)) {
  2879. return false;
  2880. }
  2881. }
  2882. return true;
  2883. }
  2884.  
  2885. /**
  2886. * @param {MetadataSearchExpression} expression
  2887. * @returns {Boolean}
  2888. */
  2889. satisfiesExpression(expression) {
  2890. const metricMap = {
  2891. "id": this.id,
  2892. "width": this.width,
  2893. "height": this.height,
  2894. "score": this.score
  2895. };
  2896. const metricValue = metricMap[expression.metric] || 0;
  2897. const value = metricMap[expression.value] || expression.value;
  2898. return this.evaluateExpression(metricValue, expression.operator, value);
  2899. }
  2900.  
  2901. /**
  2902. * @param {Number} metricValue
  2903. * @param {String} operator
  2904. * @param {Number} value
  2905. * @returns {Boolean}
  2906. */
  2907. evaluateExpression(metricValue, operator, value) {
  2908. let result = false;
  2909.  
  2910. switch (operator) {
  2911. case ":":
  2912. result = metricValue === value;
  2913. break;
  2914.  
  2915. case ":<":
  2916. result = metricValue < value;
  2917. break;
  2918.  
  2919. case ":>":
  2920. result = metricValue > value;
  2921. break;
  2922.  
  2923. default:
  2924. break;
  2925. }
  2926. return result;
  2927. }
  2928.  
  2929. addInstanceToAllMetadata() {
  2930. if (!PostMetadata.allMetadata.has(this.id)) {
  2931. PostMetadata.allMetadata.set(this.id, this);
  2932. }
  2933. }
  2934. }
  2935.  
  2936. class InactivePost {
  2937. /**
  2938. * @param {String} compressedSource
  2939. * @param {String} id
  2940. * @returns {String}
  2941. */
  2942. static decompressThumbSource(compressedSource, id) {
  2943. compressedSource = compressedSource.split("_");
  2944. return `https://us.rule34.xxx/thumbnails//${compressedSource[0]}/thumbnail_${compressedSource[1]}.jpg?${id}`;
  2945. }
  2946.  
  2947. /**
  2948. * @type {String}
  2949. */
  2950. id;
  2951. /**
  2952. * @type {String}
  2953. */
  2954. tags;
  2955. /**
  2956. * @type {String}
  2957. */
  2958. src;
  2959. /**
  2960. * @type {String}
  2961. */
  2962. metadata;
  2963. /**
  2964. * @type {Boolean}
  2965. */
  2966. fromRecord;
  2967.  
  2968. /**
  2969. * @param {HTMLElement | Object} favorite
  2970. */
  2971. constructor(favorite, fromRecord) {
  2972. this.fromRecord = fromRecord;
  2973.  
  2974. if (fromRecord) {
  2975. this.populateAttributesFromDatabaseRecord(favorite);
  2976. } else {
  2977. this.populateAttributesFromHTMLElement(favorite);
  2978. }
  2979. }
  2980.  
  2981. /**
  2982. * @param {{id: String, tags: String, src: String, metadata: String}} record
  2983. */
  2984. populateAttributesFromDatabaseRecord(record) {
  2985. this.id = record.id;
  2986. this.tags = record.tags;
  2987. this.src = InactivePost.decompressThumbSource(record.src, record.id);
  2988. this.metadata = record.metadata;
  2989. }
  2990.  
  2991. /**
  2992. * @param {HTMLElement} element
  2993. */
  2994. populateAttributesFromHTMLElement(element) {
  2995. this.id = Utils.getIdFromThumb(element);
  2996. const image = Utils.getImageFromThumb(element);
  2997.  
  2998. const source = image.src || image.getAttribute("data-cfsrc");
  2999.  
  3000. this.src = source.replace("wimg.", "");
  3001. this.tags = this.preprocessTags(image);
  3002. }
  3003.  
  3004. /**
  3005. * @param {HTMLImageElement} image
  3006. * @returns {String}
  3007. */
  3008. preprocessTags(image) {
  3009. const tags = Utils.correctMisspelledTags(image.title || image.getAttribute("tags"));
  3010. return Utils.removeExtraWhiteSpace(tags).split(" ").sort().join(" ");
  3011. }
  3012.  
  3013. instantiateMetadata() {
  3014. if (this.fromRecord) {
  3015. return new PostMetadata(this.id, this.metadata || null);
  3016. }
  3017. const favoritesMetadata = new PostMetadata(this.id);
  3018. return favoritesMetadata;
  3019. }
  3020.  
  3021. clear() {
  3022. this.id = null;
  3023. this.tags = null;
  3024. this.src = null;
  3025. this.metadata = null;
  3026. }
  3027. }
  3028.  
  3029. class Post {
  3030. /**
  3031. * @type {Map.<String, Post>}
  3032. */
  3033. static allPosts = new Map();
  3034. /**
  3035. * @type {RegExp}
  3036. */
  3037. static thumbSourceCompressionRegex = /thumbnails\/\/([0-9]+)\/thumbnail_([0-9a-f]+)/;
  3038. /**
  3039. * @type {HTMLElement}
  3040. */
  3041. static template;
  3042. /**
  3043. * @type {String}
  3044. */
  3045. static removeFavoriteButtonHTML;
  3046. /**
  3047. * @type {String}
  3048. */
  3049. static addFavoriteButtonHTML;
  3050. static currentSortingMethod = Utils.getPreference("sortingMethod", "default");
  3051. static settings = {
  3052. deferHTMLElementCreation: true
  3053. };
  3054.  
  3055. static {
  3056. Utils.addStaticInitializer(() => {
  3057. if (Utils.onFavoritesPage()) {
  3058. Post.createTemplates();
  3059. Post.addEventListeners();
  3060. }
  3061. });
  3062. }
  3063.  
  3064. static createTemplates() {
  3065. Post.removeFavoriteButtonHTML = `<img class="remove-favorite-button add-or-remove-button" src=${Utils.createObjectURLFromSvg(Utils.icons.heartMinus)}>`;
  3066. Post.addFavoriteButtonHTML = `<img class="add-favorite-button add-or-remove-button" src=${Utils.createObjectURLFromSvg(Utils.icons.heartPlus)}>`;
  3067. const buttonHTML = Utils.userIsOnTheirOwnFavoritesPage() ? Post.removeFavoriteButtonHTML : Post.addFavoriteButtonHTML;
  3068. const canvasHTML = Utils.getPerformanceProfile() > 0 ? "" : "<canvas></canvas>";
  3069. const containerTagName = "a";
  3070.  
  3071. Post.template = new DOMParser().parseFromString("", "text/html").createElement("div");
  3072. Post.template.className = Utils.favoriteItemClassName;
  3073. Post.template.innerHTML = `
  3074. <${containerTagName}>
  3075. <img>
  3076. ${buttonHTML}
  3077. ${canvasHTML}
  3078. </${containerTagName}>
  3079. `;
  3080. }
  3081.  
  3082. static addEventListeners() {
  3083. window.addEventListener("favoriteAddedOrDeleted", (event) => {
  3084. const id = event.detail;
  3085. const post = this.allPosts.get(id);
  3086.  
  3087. if (post !== undefined) {
  3088. post.swapAddOrRemoveButton();
  3089. }
  3090. });
  3091. window.addEventListener("sortingParametersChanged", () => {
  3092. Post.currentSortingMethod = Utils.getSortingMethod();
  3093. const posts = Utils.getAllThumbs().map(thumb => Post.allPosts.get(thumb.id));
  3094.  
  3095. for (const post of posts) {
  3096. post.createMetadataHint();
  3097. }
  3098. });
  3099. }
  3100.  
  3101. /**
  3102. * @param {String} id
  3103. * @returns {Number}
  3104. */
  3105. static getPixelCount(id) {
  3106. const post = Post.allPosts.get(id);
  3107.  
  3108. if (post === undefined || post.metadata === undefined) {
  3109. return 0;
  3110. }
  3111. return post.metadata.pixelCount;
  3112. }
  3113.  
  3114. /**
  3115. * @param {String} id
  3116. * @returns {String}
  3117. */
  3118. static getExtensionFromPost(id) {
  3119. const post = Post.allPosts.get(id);
  3120.  
  3121. if (post === undefined) {
  3122. return undefined;
  3123. }
  3124.  
  3125. if (post.metadata.isEmpty()) {
  3126. return undefined;
  3127. }
  3128. return post.metadata.extension;
  3129. }
  3130.  
  3131. /**
  3132. * @param {String} id
  3133. * @param {String} apiTags
  3134. * @param {String} fileURL
  3135. */
  3136. static verifyTags(id, apiTags, fileURL) {
  3137. const post = Post.allPosts.get(id);
  3138.  
  3139. if (post === undefined) {
  3140. return;
  3141. }
  3142. const postTagSet = new Set(post.originalTagSet);
  3143. const apiTagSet = Utils.convertToTagSet(apiTags);
  3144.  
  3145. if (fileURL.endsWith("mp4")) {
  3146. apiTagSet.add("video");
  3147. } else if (fileURL.endsWith("gif")) {
  3148. apiTagSet.add("gif");
  3149. } else if (!apiTagSet.has("animated_png")) {
  3150. if (apiTagSet.has("video")) {
  3151. apiTagSet.delete("video");
  3152. }
  3153.  
  3154. if (apiTagSet.has("animated")) {
  3155. apiTagSet.delete("animated");
  3156. }
  3157. }
  3158. postTagSet.delete(id);
  3159.  
  3160. if (Utils.symmetricDifference(apiTagSet, postTagSet).size > 0) {
  3161. post.initializeTags(Utils.convertToTagString(apiTagSet));
  3162. }
  3163. }
  3164.  
  3165. /**
  3166. * @type {Map.<String, Post>}
  3167. */
  3168. static get postsMatchedBySearch() {
  3169. const posts = new Map();
  3170.  
  3171. for (const [id, post] of Post.allPosts.entries()) {
  3172. if (post.matchedByMostRecentSearch) {
  3173. posts.set(id, post);
  3174. }
  3175. }
  3176. return posts;
  3177. }
  3178.  
  3179. /**
  3180. * @type {String}
  3181. */
  3182. id;
  3183. /**
  3184. * @type {HTMLDivElement}
  3185. */
  3186. root;
  3187. /**
  3188. * @type {HTMLAnchorElement}
  3189. */
  3190. container;
  3191. /**
  3192. * @type {HTMLImageElement}
  3193. */
  3194. image;
  3195. /**
  3196. * @type {HTMLImageElement}
  3197. */
  3198. addOrRemoveButton;
  3199. /**
  3200. * @type {HTMLDivElement}
  3201. */
  3202. statisticHint;
  3203. /**
  3204. * @type {InactivePost}
  3205. */
  3206. inactivePost;
  3207. /**
  3208. * @type {Boolean}
  3209. */
  3210. essentialAttributesPopulated;
  3211. /**
  3212. * @type {Boolean}
  3213. */
  3214. htmlElementCreated;
  3215. /**
  3216. * @type {Set.<String>}
  3217. */
  3218. tagSet;
  3219. /**
  3220. * @type {Set.<String>}
  3221. */
  3222. additionalTags;
  3223. /**
  3224. * @type {Number}
  3225. */
  3226. originalTagsLength;
  3227. /**
  3228. * @type {Boolean}
  3229. */
  3230. matchedByMostRecentSearch;
  3231. /**
  3232. * @type {PostMetadata}
  3233. */
  3234. metadata;
  3235.  
  3236. /**
  3237. * @type {String}
  3238. */
  3239. get href() {
  3240. return Utils.getPostPageURL(this.id);
  3241. }
  3242.  
  3243. /**
  3244. * @type {String}
  3245. */
  3246. get compressedThumbSource() {
  3247. const source = this.inactivePost === null ? this.image.src : this.inactivePost.src;
  3248. return source.match(Post.thumbSourceCompressionRegex).splice(1).join("_");
  3249. }
  3250.  
  3251. /**
  3252. * @type {{id: String, tags: String, src: String, metadata: String}}
  3253. */
  3254. get databaseRecord() {
  3255. return {
  3256. id: this.id,
  3257. tags: this.originalTagsString,
  3258. src: this.compressedThumbSource,
  3259. metadata: this.metadata.json
  3260. };
  3261. }
  3262.  
  3263. /**
  3264. * @type {Set.<String>}
  3265. */
  3266. get originalTagSet() {
  3267. const originalTags = new Set();
  3268. let count = 0;
  3269.  
  3270. for (const tag of this.tagSet.values()) {
  3271. if (count >= this.originalTagsLength) {
  3272. break;
  3273. }
  3274. count += 1;
  3275. originalTags.add(tag);
  3276. }
  3277. return originalTags;
  3278. }
  3279.  
  3280. /**
  3281. * @type {Set.<String>}
  3282. */
  3283. get originalTagsString() {
  3284. return Utils.convertToTagString(this.originalTagSet);
  3285. }
  3286.  
  3287. /**
  3288. * @type {String}
  3289. */
  3290. get additionalTagsString() {
  3291. return Utils.convertToTagString(this.additionalTags);
  3292. }
  3293.  
  3294. /**
  3295. * @param {HTMLElement | Object} thumb
  3296. * @param {Boolean} fromRecord
  3297. */
  3298. constructor(thumb, fromRecord) {
  3299. this.initializeFields();
  3300. this.initialize(new InactivePost(thumb, fromRecord));
  3301. this.setMatched(true);
  3302. this.addInstanceToAllPosts();
  3303. }
  3304.  
  3305. initializeFields() {
  3306. this.inactivePost = null;
  3307. this.essentialAttributesPopulated = false;
  3308. this.htmlElementCreated = false;
  3309. }
  3310.  
  3311. /**
  3312. * @param {InactivePost} inactivePost
  3313. */
  3314. initialize(inactivePost) {
  3315. if (Post.settings.deferHTMLElementCreation) {
  3316. this.inactivePost = inactivePost;
  3317. this.populateEssentialAttributes(inactivePost);
  3318. } else {
  3319. this.createHTMLElement(inactivePost);
  3320. }
  3321. }
  3322.  
  3323. /**
  3324. * @param {InactivePost} inactivePost
  3325. */
  3326. populateEssentialAttributes(inactivePost) {
  3327. if (this.essentialAttributesPopulated) {
  3328. return;
  3329. }
  3330. this.essentialAttributesPopulated = true;
  3331. this.id = inactivePost.id;
  3332. this.metadata = inactivePost.instantiateMetadata();
  3333. this.initializeTags(inactivePost.tags);
  3334. this.deleteConsumedProperties(inactivePost);
  3335. }
  3336.  
  3337. /**
  3338. * @param {InactivePost} inactivePost
  3339. */
  3340. createHTMLElement(inactivePost) {
  3341. if (this.htmlElementCreated) {
  3342. return;
  3343. }
  3344. this.htmlElementCreated = true;
  3345. this.instantiateTemplate();
  3346. this.populateEssentialAttributes(inactivePost);
  3347. this.populateHTMLAttributes(inactivePost);
  3348. this.setupAddOrRemoveButton(Utils.userIsOnTheirOwnFavoritesPage());
  3349. this.setupClickLink();
  3350. this.deleteInactivePost();
  3351. }
  3352.  
  3353. activateHTMLElement() {
  3354. if (this.inactivePost !== null) {
  3355. this.createHTMLElement(this.inactivePost);
  3356. }
  3357. }
  3358.  
  3359. instantiateTemplate() {
  3360. this.root = Post.template.cloneNode(true);
  3361. this.container = this.root.children[0];
  3362. this.image = this.root.children[0].children[0];
  3363. this.addOrRemoveButton = this.root.children[0].children[1];
  3364. }
  3365.  
  3366. /**
  3367. * @param {Boolean} isRemoveButton
  3368. */
  3369. setupAddOrRemoveButton(isRemoveButton) {
  3370. if (isRemoveButton) {
  3371. this.addOrRemoveButton.onmousedown = (event) => {
  3372. event.stopPropagation();
  3373.  
  3374. if (event.button === Utils.clickCodes.left) {
  3375. this.removeFavorite();
  3376. }
  3377. };
  3378. } else {
  3379. this.addOrRemoveButton.onmousedown = (event) => {
  3380. event.stopPropagation();
  3381.  
  3382. if (event.button === Utils.clickCodes.left) {
  3383. this.addFavorite();
  3384. }
  3385. };
  3386. }
  3387. }
  3388.  
  3389. removeFavorite() {
  3390. Utils.removeFavorite(this.id);
  3391. this.swapAddOrRemoveButton();
  3392. }
  3393.  
  3394. addFavorite() {
  3395. Utils.addFavorite(this.id);
  3396. this.swapAddOrRemoveButton();
  3397. }
  3398.  
  3399. swapAddOrRemoveButton() {
  3400. const isRemoveButton = this.addOrRemoveButton.classList.contains("remove-favorite-button");
  3401.  
  3402. this.addOrRemoveButton.outerHTML = isRemoveButton ? Post.addFavoriteButtonHTML : Post.removeFavoriteButtonHTML;
  3403. this.addOrRemoveButton = this.root.children[0].children[1];
  3404. this.setupAddOrRemoveButton(!isRemoveButton);
  3405. }
  3406.  
  3407. /**
  3408. * @param {InactivePost} inactivePost
  3409. */
  3410. async populateHTMLAttributes(inactivePost) {
  3411. this.image.src = inactivePost.src;
  3412. this.image.classList.add(Utils.getContentType(inactivePost.tags || Utils.convertToTagString(this.tagSet)));
  3413. this.root.id = inactivePost.id;
  3414.  
  3415. if (!Utils.onMobileDevice()) {
  3416. this.container.href = await Utils.getOriginalImageURLWithExtension(this.root);
  3417. }
  3418. }
  3419.  
  3420. /**
  3421. * @param {String} tags
  3422. */
  3423. initializeTags(tags) {
  3424. this.tagSet = Utils.convertToTagSet(`${this.id} ${tags}`);
  3425. this.originalTagsLength = this.tagSet.size;
  3426. this.initializeAdditionalTags();
  3427. }
  3428.  
  3429. initializeAdditionalTags() {
  3430. this.additionalTags = Utils.convertToTagSet(TagModifier.tagModifications.get(this.id) || "");
  3431.  
  3432. if (this.additionalTags.size !== 0) {
  3433. this.combineOriginalAndAdditionalTags();
  3434. }
  3435. }
  3436.  
  3437. /**
  3438. * @param {InactivePost} inactivePost
  3439. */
  3440. deleteConsumedProperties(inactivePost) {
  3441. inactivePost.metadata = null;
  3442. inactivePost.tags = null;
  3443. }
  3444.  
  3445. setupClickLink() {
  3446. if (!Utils.onFavoritesPage()) {
  3447. return;
  3448. }
  3449. this.container.addEventListener("mousedown", (event) => {
  3450. if (event.ctrlKey) {
  3451. return;
  3452. }
  3453. const middleClick = event.button === Utils.clickCodes.middle;
  3454. const leftClick = event.button === Utils.clickCodes.left;
  3455. const shiftClick = leftClick && event.shiftKey;
  3456.  
  3457. if (middleClick || shiftClick || (leftClick && !Utils.galleryEnabled())) {
  3458. Utils.openPostInNewTab(this.id);
  3459. }
  3460. });
  3461. }
  3462.  
  3463. deleteInactivePost() {
  3464. if (this.inactivePost !== null) {
  3465. this.inactivePost.clear();
  3466. this.inactivePost = null;
  3467. }
  3468. }
  3469.  
  3470. /**
  3471. * @param {HTMLElement} content
  3472. */
  3473. insertAtEndOfContent(content) {
  3474. if (this.inactivePost !== null) {
  3475. this.createHTMLElement(this.inactivePost, true);
  3476. }
  3477. this.createMetadataHint();
  3478. content.appendChild(this.root);
  3479. }
  3480.  
  3481. /**
  3482. * @param {HTMLElement} content
  3483. */
  3484. insertAtBeginningOfContent(content) {
  3485. if (this.inactivePost !== null) {
  3486. this.createHTMLElement(this.inactivePost, true);
  3487. }
  3488. this.createMetadataHint();
  3489. content.insertAdjacentElement("afterbegin", this.root);
  3490. }
  3491.  
  3492. addInstanceToAllPosts() {
  3493. if (!Post.allPosts.has(this.id)) {
  3494. Post.allPosts.set(this.id, this);
  3495. }
  3496. }
  3497.  
  3498. toggleMatched() {
  3499. this.matchedByMostRecentSearch = !this.matchedByMostRecentSearch;
  3500. }
  3501.  
  3502. /**
  3503. * @param {Boolean} value
  3504. */
  3505. setMatched(value) {
  3506. this.matchedByMostRecentSearch = value;
  3507. }
  3508.  
  3509. combineOriginalAndAdditionalTags() {
  3510. this.tagSet = this.originalTagSet;
  3511. this.tagSet = Utils.union(this.tagSet, this.additionalTags);
  3512. }
  3513.  
  3514. /**
  3515. * @param {String} newTags
  3516. * @returns {String}
  3517. */
  3518. addAdditionalTags(newTags) {
  3519. const newTagsSet = Utils.convertToTagSet(newTags);
  3520.  
  3521. if (newTagsSet.size > 0) {
  3522. this.additionalTags = Utils.union(this.additionalTags, newTagsSet);
  3523. this.combineOriginalAndAdditionalTags();
  3524. }
  3525. return this.additionalTagsString;
  3526. }
  3527.  
  3528. /**
  3529. * @param {String} tagsToRemove
  3530. * @returns {String}
  3531. */
  3532. removeAdditionalTags(tagsToRemove) {
  3533. const tagsToRemoveSet = Utils.convertToTagSet(tagsToRemove);
  3534.  
  3535. if (tagsToRemoveSet.size > 0) {
  3536. this.additionalTags = Utils.difference(this.additionalTags, tagsToRemoveSet);
  3537. this.combineOriginalAndAdditionalTags();
  3538. }
  3539. return this.additionalTagsString;
  3540. }
  3541.  
  3542. resetAdditionalTags() {
  3543. if (this.additionalTags.size === 0) {
  3544. return;
  3545. }
  3546. this.additionalTags = new Set();
  3547. this.combineOriginalAndAdditionalTags();
  3548. }
  3549.  
  3550. /**
  3551. * @returns {HTMLDivElement}
  3552. */
  3553. getMetadataHintElement() {
  3554. return this.container.querySelector(".statistic-hint");
  3555. }
  3556.  
  3557. /**
  3558. * @returns {Boolean}
  3559. */
  3560. hasStatisticHint() {
  3561. return this.getMetadataHintElement() !== null;
  3562. }
  3563.  
  3564. /**
  3565. * @returns {String}
  3566. */
  3567. getMetadataHintValue() {
  3568. switch (Post.currentSortingMethod) {
  3569. case "score":
  3570. return this.metadata.score;
  3571.  
  3572. case "width":
  3573. return this.metadata.width;
  3574.  
  3575. case "height":
  3576. return this.metadata.height;
  3577.  
  3578. case "create":
  3579. return Utils.convertTimestampToDate(this.metadata.creationTimestamp);
  3580.  
  3581. case "change":
  3582. return Utils.convertTimestampToDate(this.metadata.lastChangedTimestamp * 1000);
  3583.  
  3584. default:
  3585. return this.id;
  3586. }
  3587. }
  3588.  
  3589. async createMetadataHint() {
  3590. // await sleep(200);
  3591. // let hint = this.getMetadataHintElement();
  3592.  
  3593. // if (hint === null) {
  3594. // hint = document.createElement("div");
  3595. // hint.className = "statistic-hint";
  3596. // this.container.appendChild(hint);
  3597. // }
  3598. // hint.textContent = this.getMetadataHintValue();
  3599. }
  3600. }
  3601.  
  3602. class SearchTag {
  3603. /**
  3604. * @type {String}
  3605. */
  3606. value;
  3607. /**
  3608. * @type {Boolean}
  3609. */
  3610. negated;
  3611.  
  3612. /**
  3613. * @type {Number}
  3614. */
  3615. get cost() {
  3616. return 0;
  3617. }
  3618.  
  3619. /**
  3620. * @type {Number}
  3621. */
  3622. get finalCost() {
  3623. return this.negated ? this.cost + 1 : this.cost;
  3624. }
  3625.  
  3626. /**
  3627. * @param {String} searchTag
  3628. */
  3629. constructor(searchTag) {
  3630. this.negated = searchTag.startsWith("-");
  3631. this.value = this.negated ? searchTag.substring(1) : searchTag;
  3632. }
  3633.  
  3634. /**
  3635. * @param {Post} post
  3636. * @returns {Boolean}
  3637. */
  3638. matches(post) {
  3639. if (post.tagSet.has(this.value)) {
  3640. return !this.negated;
  3641. }
  3642. return this.negated;
  3643. }
  3644. }
  3645.  
  3646. class WildcardSearchTag extends SearchTag {
  3647. static unmatchableRegex = /^\b$/;
  3648. static startsWithRegex = /^[^*]*\*$/;
  3649.  
  3650. /**
  3651. * @type {RegExp}
  3652. */
  3653. matchRegex;
  3654. /**
  3655. * @type {Boolean}
  3656. */
  3657. equivalentToStartsWith;
  3658. /**
  3659. * @type {String}
  3660. */
  3661. startsWithPrefix;
  3662.  
  3663. /**
  3664. * @type {Number}
  3665. */
  3666. get cost() {
  3667. return this.equivalentToStartsWith ? 10 : 20;
  3668. }
  3669.  
  3670. /**
  3671. * @param {String} searchTag
  3672. */
  3673. constructor(searchTag) {
  3674. super(searchTag);
  3675. this.matchRegex = this.createWildcardRegex();
  3676. this.startsWithPrefix = this.value.slice(0, -1);
  3677. this.equivalentToStartsWith = WildcardSearchTag.startsWithRegex.test(searchTag);
  3678. this.matches = this.equivalentToStartsWith ? this.matchesPrefix : this.matchesWildcard;
  3679. }
  3680.  
  3681. /**
  3682. * @returns {RegExp}
  3683. */
  3684. createWildcardRegex() {
  3685. try {
  3686. return new RegExp(`^${this.value.replaceAll(/\*/g, ".*")}$`);
  3687. } catch {
  3688. return WildcardSearchTag.unmatchableRegex;
  3689. }
  3690. }
  3691.  
  3692. /**
  3693. * @param {Post} post
  3694. * @returns {Boolean}
  3695. */
  3696. matchesPrefix(post) {
  3697. for (const tag of post.tagSet.values()) {
  3698. if (tag.startsWith(this.startsWithPrefix)) {
  3699. return !this.negated;
  3700. }
  3701.  
  3702. if (this.startsWithPrefix < tag) {
  3703. break;
  3704. }
  3705. }
  3706. return this.negated;
  3707. }
  3708.  
  3709. /**
  3710. * @param {Post} post
  3711. * @returns {Boolean}
  3712. */
  3713. matchesWildcard(post) {
  3714. for (const tag of post.tagSet.values()) {
  3715. if (this.matchRegex.test(tag)) {
  3716. return !this.negated;
  3717. }
  3718. }
  3719. return this.negated;
  3720. }
  3721. }
  3722.  
  3723. class MetadataSearchTag extends SearchTag {
  3724. static regex = /^-?(score|width|height|id)(:[<>]?)(\d+|score|width|height|id)$/;
  3725.  
  3726. /**
  3727. * @type {MetadataSearchExpression}
  3728. */
  3729. expression;
  3730.  
  3731. /**
  3732. * @type {Number}
  3733. */
  3734. get cost() {
  3735. return 0;
  3736. }
  3737.  
  3738. /**
  3739. * @param {String} searchTag
  3740. * @param {Boolean} inOrGroup
  3741. */
  3742. constructor(searchTag) {
  3743. super(searchTag);
  3744. this.expression = this.createExpression(searchTag);
  3745. }
  3746.  
  3747. /**
  3748. * @param {String} searchTag
  3749. * @returns {MetadataSearchExpression}
  3750. */
  3751. createExpression(searchTag) {
  3752. const extractedExpression = MetadataSearchTag.regex.exec(searchTag);
  3753. return new MetadataSearchExpression(
  3754. extractedExpression[1],
  3755. extractedExpression[2],
  3756. extractedExpression[3]
  3757. );
  3758. }
  3759.  
  3760. /**
  3761. * @param {Post} post
  3762. * @returns {Boolean}
  3763. */
  3764. matches(post) {
  3765. if (post.metadata.satisfiesExpression(this.expression)) {
  3766. return !this.negated;
  3767. }
  3768. return this.negated;
  3769. }
  3770. }
  3771.  
  3772. class SearchCommand {
  3773. /**
  3774. * @param {String} tag
  3775. * @returns {SearchTag}
  3776. */
  3777. static createSearchTag(tag) {
  3778. if (MetadataSearchTag.regex.test(tag)) {
  3779. return new MetadataSearchTag(tag);
  3780. }
  3781.  
  3782. if (tag.includes("*")) {
  3783. return new WildcardSearchTag(tag);
  3784. }
  3785. return new SearchTag(tag);
  3786. }
  3787.  
  3788. /**
  3789. * @param {String[]} tags
  3790. * @param {Boolean} isOrGroup
  3791. * @returns {SearchTag[]}
  3792. */
  3793. static createSearchTagGroup(tags) {
  3794. const uniqueTags = new Set();
  3795. const searchTags = [];
  3796.  
  3797. for (const tag of tags) {
  3798. if (!uniqueTags.has(tag)) {
  3799. uniqueTags.add(tag);
  3800. searchTags.push(SearchCommand.createSearchTag(tag));
  3801. }
  3802. }
  3803. return searchTags;
  3804. }
  3805.  
  3806. /**
  3807. * @param {SearchTag[]} searchTags
  3808. */
  3809. static sortByLeastExpensive(searchTags) {
  3810. searchTags.sort((a, b) => {
  3811. return a.finalCost - b.finalCost;
  3812. });
  3813. }
  3814.  
  3815. /**
  3816. * @type {SearchTag[][]}
  3817. */
  3818. orGroups;
  3819. /**
  3820. * @type {SearchTag[]}
  3821. */
  3822. remainingSearchTags;
  3823. /**
  3824. * @type {Boolean}
  3825. */
  3826. isEmpty;
  3827.  
  3828. /**
  3829. * @param {String} searchQuery
  3830. */
  3831. constructor(searchQuery) {
  3832. this.isEmpty = searchQuery.trim() === "";
  3833.  
  3834. if (this.isEmpty) {
  3835. return;
  3836. }
  3837. const {orGroups, remainingSearchTags} = Utils.extractTagGroups(searchQuery);
  3838.  
  3839. this.orGroups = orGroups.map(orGroup => SearchCommand.createSearchTagGroup(orGroup));
  3840. this.remainingSearchTags = SearchCommand.createSearchTagGroup(remainingSearchTags);
  3841. this.optimizeSearchCommand();
  3842. }
  3843.  
  3844. optimizeSearchCommand() {
  3845. for (const orGroup of this.orGroups) {
  3846. SearchCommand.sortByLeastExpensive(orGroup);
  3847. }
  3848. SearchCommand.sortByLeastExpensive(this.remainingSearchTags);
  3849. this.orGroups.sort((a, b) => {
  3850. return a.length - b.length;
  3851. });
  3852. }
  3853.  
  3854. /**
  3855. * @param {Post} post
  3856. * @returns {Boolean}
  3857. */
  3858. matches(post) {
  3859. if (this.isEmpty) {
  3860. return true;
  3861. }
  3862.  
  3863. if (!this.matchesAllRemainingSearchTags(post)) {
  3864. return false;
  3865. }
  3866. return this.matchesAllOrGroups(post);
  3867. }
  3868.  
  3869. /**
  3870. * @param {Post} post
  3871. * @returns {Boolean}
  3872. */
  3873. matchesAllRemainingSearchTags(post) {
  3874. for (const searchTag of this.remainingSearchTags) {
  3875. if (!searchTag.matches(post)) {
  3876. return false;
  3877. }
  3878. }
  3879. return true;
  3880. }
  3881.  
  3882. /**
  3883. * @param {Post} post
  3884. * @returns {Boolean}
  3885. */
  3886. matchesAllOrGroups(post) {
  3887. for (const orGroup of this.orGroups) {
  3888. if (!this.atLeastOnePostTagIsInOrGroup(orGroup, post)) {
  3889. return false;
  3890. }
  3891. }
  3892. return true;
  3893. }
  3894.  
  3895. /**
  3896. * @param {SearchTag[]} orGroup
  3897. * @param {Post} post
  3898. * @returns {Boolean}
  3899. */
  3900. atLeastOnePostTagIsInOrGroup(orGroup, post) {
  3901. for (const orTag of orGroup) {
  3902. if (orTag.matches(post)) {
  3903. return true;
  3904. }
  3905. }
  3906. return false;
  3907. }
  3908. }
  3909.  
  3910. class FavoritesPageRequest {
  3911. /**
  3912. * @type {Number}
  3913. */
  3914. pageNumber;
  3915. /**
  3916. * @type {Number}
  3917. */
  3918. retryCount;
  3919. /**
  3920. * @type {Post[]}
  3921. */
  3922. fetchedFavorites;
  3923.  
  3924. /**
  3925. * @type {String}
  3926. */
  3927. get url() {
  3928. return `${document.location.href}&pid=${this.pageNumber * 50}`;
  3929. }
  3930.  
  3931. /**
  3932. * @type {Number}
  3933. */
  3934. get retryDelay() {
  3935. return (7 ** (this.retryCount)) + 200;
  3936. }
  3937.  
  3938. /**
  3939. * @param {Number} pageNumber
  3940. */
  3941. constructor(pageNumber) {
  3942. this.pageNumber = pageNumber;
  3943. this.retryCount = 0;
  3944. this.fetchedFavorites = [];
  3945. }
  3946.  
  3947. onFail() {
  3948. this.retryCount += 1;
  3949. }
  3950. }
  3951.  
  3952. class FavoritesParser {
  3953. static parser = new DOMParser();
  3954.  
  3955. /**
  3956. * @param {String} favoritesPageHTML
  3957. * @returns {Post[]}
  3958. */
  3959. static extractFavorites(favoritesPageHTML) {
  3960. const elements = FavoritesParser.extractFavoriteElements(favoritesPageHTML);
  3961. return elements.map(element => new Post(element, false));
  3962. }
  3963.  
  3964. /**
  3965. * @param {String} favoritesPageHTML
  3966. * @returns {HTMLElement[]}
  3967. */
  3968. static extractFavoriteElements(favoritesPageHTML) {
  3969. const dom = FavoritesParser.parser.parseFromString(favoritesPageHTML, "text/html");
  3970. const thumbs = Array.from(dom.getElementsByClassName("thumb"));
  3971.  
  3972. if (thumbs.length > 0) {
  3973. return thumbs;
  3974. }
  3975. return Array.from(dom.getElementsByTagName("img"))
  3976. .filter(image => image.src.includes("thumbnail_"))
  3977. .map(image => image.parentElement);
  3978. }
  3979. }
  3980.  
  3981. class FetchedFavoritesQueue {
  3982. /**
  3983. * @type {FavoritesPageRequest[]}
  3984. */
  3985. queue;
  3986. /**
  3987. * @type {Function}
  3988. */
  3989. onDequeue;
  3990. /**
  3991. * @type {Number}
  3992. */
  3993. lastDequeuedPageNumber;
  3994. /**
  3995. * @type {Boolean}
  3996. */
  3997. dequeuing;
  3998.  
  3999. /**
  4000. * @type {Number}
  4001. */
  4002. get lowestEnqueuedPageNumber() {
  4003. return this.queue[0].pageNumber;
  4004. }
  4005.  
  4006. /**
  4007. * @type {Number}
  4008. */
  4009. get nextPageNumberToDequeue() {
  4010. return this.lastDequeuedPageNumber + 1;
  4011. }
  4012.  
  4013. /**
  4014. * @type {Boolean}
  4015. */
  4016. get allPreviousPagesWereDequeued() {
  4017. return this.nextPageNumberToDequeue === this.lowestEnqueuedPageNumber;
  4018. }
  4019.  
  4020. /**
  4021. * @type {Boolean}
  4022. */
  4023. get isEmpty() {
  4024. return this.queue.length === 0;
  4025. }
  4026.  
  4027. /**
  4028. * @type {Boolean}
  4029. */
  4030. get canDequeue() {
  4031. return !this.isEmpty && this.allPreviousPagesWereDequeued;
  4032. }
  4033.  
  4034. /**
  4035. * @param {Function}
  4036. */
  4037. constructor(onDequeue) {
  4038. this.onDequeue = onDequeue;
  4039. this.lastDequeuedPageNumber = -1;
  4040. this.queue = [];
  4041. }
  4042.  
  4043. /**
  4044. * @param {FavoritesPageRequest} request
  4045. */
  4046. enqueue(request) {
  4047. this.queue.push(request);
  4048. this.sortByLowestPageNumber();
  4049. this.dequeueAll();
  4050. }
  4051.  
  4052. sortByLowestPageNumber() {
  4053. this.queue.sort((request1, request2) => request1.pageNumber - request2.pageNumber);
  4054. }
  4055.  
  4056. dequeueAll() {
  4057. if (this.dequeuing) {
  4058. return;
  4059. }
  4060. this.dequeuing = true;
  4061.  
  4062. while (this.canDequeue) {
  4063. this.dequeue();
  4064. }
  4065. this.dequeuing = false;
  4066. }
  4067.  
  4068. dequeue() {
  4069. this.lastDequeuedPageNumber += 1;
  4070. this.onDequeue(this.queue.shift());
  4071. }
  4072. }
  4073.  
  4074. class FavoritesFetcher {
  4075. /**
  4076. * @type {Function}
  4077. */
  4078. onAllRequestsCompleted;
  4079. /**
  4080. * @type {Function}
  4081. */
  4082. onRequestCompleted;
  4083. /**
  4084. * @type {Set.<Number>}
  4085. */
  4086. pendingRequestPageNumbers;
  4087. /**
  4088. * @type {FavoritesPageRequest[]}
  4089. */
  4090. failedRequests;
  4091. /**
  4092. * @type {Set.<String>}
  4093. */
  4094. storedFavoriteIds;
  4095. /**
  4096. * @type {Number}
  4097. */
  4098. currentPageNumber;
  4099. /**
  4100. * @type {Boolean}
  4101. */
  4102. fetchedAnEmptyPage;
  4103.  
  4104. /**
  4105. * @type {Boolean}
  4106. */
  4107. get hasFailedRequests() {
  4108. return this.failedRequests.length > 0;
  4109. }
  4110.  
  4111. /**
  4112. * @type {Boolean}
  4113. */
  4114. get allRequestsHaveStarted() {
  4115. return this.fetchedAnEmptyPage;
  4116. }
  4117.  
  4118. /**
  4119. * @type {Boolean}
  4120. */
  4121. get someRequestsArePending() {
  4122. return this.pendingRequestPageNumbers.size > 0 || this.hasFailedRequests;
  4123. }
  4124.  
  4125. /**
  4126. * @type {Boolean}
  4127. */
  4128. get allRequestsHaveCompleted() {
  4129. return this.allRequestsHaveStarted && !this.someRequestsArePending;
  4130. }
  4131.  
  4132. /**
  4133. * @type {FavoritesPageRequest}
  4134. */
  4135. get oldestFailedFetchRequest() {
  4136. return this.failedRequests.shift();
  4137. }
  4138.  
  4139. /**
  4140. * @type {FavoritesPageRequest}
  4141. */
  4142. get newFetchRequest() {
  4143. const request = new FavoritesPageRequest(this.currentPageNumber);
  4144.  
  4145. this.pendingRequestPageNumbers.add(request.pageNumber);
  4146. this.currentPageNumber += 1;
  4147. return request;
  4148. }
  4149.  
  4150. /**
  4151. * @type {FavoritesPageRequest | null}
  4152. */
  4153. get nextFetchRequest() {
  4154. if (this.hasFailedRequests) {
  4155. return this.oldestFailedFetchRequest;
  4156. }
  4157.  
  4158. if (!this.allRequestsHaveStarted) {
  4159. return this.newFetchRequest;
  4160. }
  4161. return null;
  4162. }
  4163.  
  4164. /**
  4165. * @param {Function} onAllRequestsCompleted
  4166. * @param {Function} onRequestCompleted
  4167. */
  4168. constructor(onAllRequestsCompleted, onRequestCompleted) {
  4169. this.onAllRequestsCompleted = onAllRequestsCompleted;
  4170. this.onRequestCompleted = onRequestCompleted;
  4171. this.storedFavoriteIds = new Set();
  4172. this.pendingRequestPageNumbers = new Set();
  4173. this.failedRequests = [];
  4174. this.currentPageNumber = 0;
  4175. this.fetchedAnEmptyPage = false;
  4176. }
  4177.  
  4178. async fetchAllFavorites() {
  4179. while (!this.allRequestsHaveCompleted) {
  4180. await this.fetchFavoritesPage(this.nextFetchRequest);
  4181. }
  4182. this.onAllRequestsCompleted();
  4183. }
  4184.  
  4185. /**
  4186. * @param {Set.<String>} storedFavoriteIds
  4187. */
  4188. async fetchAllNewFavoritesOnReload(storedFavoriteIds) {
  4189. this.storedFavoriteIds = storedFavoriteIds;
  4190. let favorites = [];
  4191.  
  4192. while (true) {
  4193. const {allNewFavoritesFound, newFavorites} = await this.fetchNewFavoritesOnReload();
  4194.  
  4195. favorites = favorites.concat(newFavorites);
  4196.  
  4197. if (allNewFavoritesFound) {
  4198. this.storedFavoriteIds = null;
  4199. this.onAllRequestsCompleted(favorites);
  4200. return;
  4201. }
  4202. }
  4203. }
  4204.  
  4205. /**
  4206. * @returns {Promise.<{allNewFavoritesFound: Boolean, newFavorites: Post[]}>}
  4207. */
  4208. fetchNewFavoritesOnReload() {
  4209. return fetch(this.newFetchRequest.url)
  4210. .then((response) => {
  4211. return response.text();
  4212. })
  4213. .then((html) => {
  4214. return this.extractNewFavorites(html);
  4215. });
  4216. }
  4217.  
  4218. /**
  4219. * @param {String} html
  4220. * @returns {{allNewFavoritesFound: Boolean, newFavorites: Post[]}}
  4221. */
  4222. extractNewFavorites(html) {
  4223. const newFavorites = [];
  4224. const fetchedFavorites = FavoritesParser.extractFavorites(html);
  4225. let allNewFavoritesFound = fetchedFavorites.length === 0;
  4226.  
  4227. for (const favorite of fetchedFavorites) {
  4228. if (this.storedFavoriteIds.has(favorite.id)) {
  4229. allNewFavoritesFound = true;
  4230. break;
  4231. }
  4232. newFavorites.push(favorite);
  4233. }
  4234. return {
  4235. allNewFavoritesFound,
  4236. newFavorites
  4237. };
  4238. }
  4239.  
  4240. /**
  4241. * @param {FavoritesPageRequest} request
  4242. */
  4243. async fetchFavoritesPage(request) {
  4244. if (request === null) {
  4245. await Utils.sleep(200);
  4246. return;
  4247. }
  4248. fetch(request.url)
  4249. .then((response) => {
  4250. return this.onFavoritesPageRequestResponse(response);
  4251. })
  4252. .then((html) => {
  4253. this.onFavoritesPageRequestSuccess(request, html);
  4254. })
  4255. .catch((error) => {
  4256. this.onFavoritesPageRequestFail(request, error);
  4257. });
  4258. await Utils.sleep(request.retryDelay);
  4259. }
  4260.  
  4261. /**
  4262. * @param {Response} response
  4263. * @returns {Promise.<String>}
  4264. */
  4265. onFavoritesPageRequestResponse(response) {
  4266. if (response.ok) {
  4267. return response.text();
  4268. }
  4269. throw new Error(`${response.status}: Failed to fetch, ${response.url}`);
  4270. }
  4271.  
  4272. /**
  4273. * @param {FavoritesPageRequest} request
  4274. * @param {String} html
  4275. */
  4276. onFavoritesPageRequestSuccess(request, html) {
  4277. request.fetchedFavorites = FavoritesParser.extractFavorites(html);
  4278. this.pendingRequestPageNumbers.delete(request.pageNumber);
  4279. const favoritesPageIsEmpty = request.fetchedFavorites.length === 0;
  4280.  
  4281. this.fetchedAnEmptyPage = this.fetchedAnEmptyPage || favoritesPageIsEmpty;
  4282.  
  4283. if (!favoritesPageIsEmpty) {
  4284. this.onRequestCompleted(request);
  4285. }
  4286. }
  4287.  
  4288. /**
  4289. * @param {FavoritesPageRequest} request
  4290. * @param {Error} error
  4291. */
  4292. onFavoritesPageRequestFail(request, error) {
  4293. console.error(error);
  4294. request.onFail();
  4295. this.failedRequests.push(request);
  4296. }
  4297. }
  4298.  
  4299. class FavoritesPaginator {
  4300. /**
  4301. * @type {HTMLDivElement}
  4302. */
  4303. content;
  4304. /**
  4305. * @type {HTMLElement}
  4306. */
  4307. paginationMenu;
  4308. /**
  4309. * @type {HTMLLabelElement}
  4310. */
  4311. paginationLabel;
  4312. /**
  4313. * @type {Number}
  4314. */
  4315. currentPageNumber;
  4316. /**
  4317. * @type {Number}
  4318. */
  4319. maxFavoritesPerPage;
  4320. /**
  4321. * @type {Number}
  4322. */
  4323. maxPageNumberButtons;
  4324.  
  4325. constructor() {
  4326. this.content = this.createContentContainer();
  4327. this.paginationMenu = this.createPaginationMenuContainer();
  4328. this.currentPageNumber = 1;
  4329. this.favoritesPerPage = Utils.getPreference("resultsPerPage", Utils.defaults.resultsPerPage);
  4330. this.maxPageNumberButtons = Utils.onMobileDevice() ? 4 : 5;
  4331. }
  4332.  
  4333. /**
  4334. * @returns {HTMLDivElement}
  4335. */
  4336. createContentContainer() {
  4337. const content = document.createElement("div");
  4338.  
  4339. content.id = "favorites-search-gallery-content";
  4340. Utils.favoritesSearchGalleryContainer.appendChild(content);
  4341. return content;
  4342. }
  4343.  
  4344. /**
  4345. * @returns {HTMLDivElement}
  4346. */
  4347. createPaginationMenuContainer() {
  4348. const container = document.createElement("span");
  4349.  
  4350. container.id = "favorites-pagination-container";
  4351. return container;
  4352. }
  4353.  
  4354. insertPaginationMenuContainer() {
  4355. if (document.getElementById(this.paginationMenu.id) === null) {
  4356. const placeToInsertPagination = document.getElementById("favorites-pagination-placeholder");
  4357.  
  4358. placeToInsertPagination.insertAdjacentElement("afterend", this.paginationMenu);
  4359. placeToInsertPagination.remove();
  4360. }
  4361.  
  4362. }
  4363.  
  4364. /**
  4365. * @param {Post[]} favorites
  4366. */
  4367. paginate(favorites) {
  4368. this.insertPaginationMenuContainer();
  4369. this.changePage(1, favorites);
  4370. }
  4371.  
  4372. /**
  4373. * @param {Post[]} favorites
  4374. */
  4375. paginateWhileFetching(favorites) {
  4376. const pageNumberButtons = document.getElementsByClassName("pagination-number");
  4377. const lastPageButtonNumber = pageNumberButtons.length > 0 ? parseInt(pageNumberButtons[pageNumberButtons.length - 1].textContent) : 1;
  4378. const pageCount = this.getPageCount(favorites.length);
  4379. const needsToCreateNewPage = pageCount > lastPageButtonNumber;
  4380. const nextPageButton = document.getElementById("next-page");
  4381. const alreadyAtMaxPageNumberButtons = document.getElementsByClassName("pagination-number").length >= this.maxPageNumberButtons &&
  4382. nextPageButton !== null && nextPageButton.style.display !== "none" &&
  4383. nextPageButton.style.visibility !== "hidden" && !nextPageButton.disabled;
  4384.  
  4385. if (needsToCreateNewPage && !alreadyAtMaxPageNumberButtons) {
  4386. this.createPaginationMenu(this.currentPageNumber, favorites);
  4387. } else {
  4388. this.updateTraversalButtonEventListeners(favorites);
  4389. this.updatePageNumberButtonEventListeners(favorites);
  4390. }
  4391. const onLastPage = (pageCount === this.currentPageNumber);
  4392.  
  4393. if (!onLastPage) {
  4394. return;
  4395. }
  4396. const range = this.getPaginationRange(this.currentPageNumber);
  4397. const favoritesToAdd = favorites.slice(range.start, range.end)
  4398. .filter(favorite => document.getElementById(favorite.id) === null);
  4399.  
  4400. for (const favorite of favoritesToAdd) {
  4401. favorite.insertAtEndOfContent(this.content);
  4402. }
  4403. this.setPaginationLabel(this.currentPageNumber, favorites.length);
  4404. }
  4405.  
  4406. /**
  4407. * @param {Number} pageNumber
  4408. * @param {Post[]} favorites
  4409. */
  4410. changePage(pageNumber, favorites) {
  4411. this.currentPageNumber = pageNumber;
  4412. this.createPaginationMenu(pageNumber, favorites);
  4413. this.showFavorites(pageNumber, favorites);
  4414.  
  4415. if (FavoritesLoader.currentState !== FavoritesLoader.states.loadingFavoritesFromDatabase) {
  4416. dispatchEvent(new Event("changedPage"));
  4417. }
  4418.  
  4419. if (Utils.onMobileDevice()) {
  4420. this.paginationMenu.blur();
  4421. }
  4422. }
  4423.  
  4424. /**
  4425. * @param {Number} pageNumber
  4426. * @param {Post[]} favorites
  4427. */
  4428. createPaginationMenu(pageNumber, favorites) {
  4429. this.paginationMenu.innerHTML = "";
  4430. this.setPaginationLabel(pageNumber, favorites.length);
  4431. this.createPageNumberButtons(pageNumber, favorites);
  4432. this.createPageTraversalButtons(favorites);
  4433. this.createGotoSpecificPageInputs(favorites);
  4434. }
  4435.  
  4436. /**
  4437. * @param {Number} pageNumber
  4438. * @param {Number} favoriteCount
  4439. */
  4440. setPaginationLabel(pageNumber, favoriteCount) {
  4441. const range = this.getPaginationRange(pageNumber);
  4442. const start = range.start;
  4443. const end = Math.min(range.end, favoriteCount);
  4444.  
  4445. if (this.paginationLabel === undefined) {
  4446. this.paginationLabel = document.getElementById("pagination-label");
  4447. }
  4448.  
  4449. if (favoriteCount <= this.maxFavoritesPerPage || isNaN(start) || isNaN(end)) {
  4450. this.paginationLabel.textContent = "";
  4451. return;
  4452. }
  4453. this.paginationLabel.textContent = `${start + 1} - ${end}`;
  4454. }
  4455.  
  4456. /**
  4457. * @param {Number} pageNumber
  4458. * @returns {{start: Number, end: Number}}
  4459. */
  4460. getPaginationRange(pageNumber) {
  4461. return {
  4462. start: this.maxFavoritesPerPage * (pageNumber - 1),
  4463. end: this.maxFavoritesPerPage * pageNumber
  4464. };
  4465. }
  4466.  
  4467. /**
  4468. * @param {Number} favoriteCount
  4469. * @returns {Number}
  4470. */
  4471. getPageCount(favoriteCount) {
  4472. if (favoriteCount === 0) {
  4473. return 1;
  4474. }
  4475. const pageCount = favoriteCount / this.maxFavoritesPerPage;
  4476.  
  4477. if (favoriteCount % this.maxFavoritesPerPage === 0) {
  4478. return pageCount;
  4479. }
  4480. return Math.floor(pageCount) + 1;
  4481. }
  4482.  
  4483. /**
  4484. * @param {Number} pageNumber
  4485. * @param {Post[]} favorites
  4486. */
  4487. createPageNumberButtons(pageNumber, favorites) {
  4488. const pageCount = this.getPageCount(favorites.length);
  4489. let numberOfButtonsCreated = 0;
  4490.  
  4491. for (let i = pageNumber; i <= pageCount && numberOfButtonsCreated < this.maxPageNumberButtons; i += 1) {
  4492. numberOfButtonsCreated += 1;
  4493. this.createPageNumberButton(pageNumber, i, favorites);
  4494. }
  4495.  
  4496. if (numberOfButtonsCreated >= this.maxPageNumberButtons) {
  4497. return;
  4498. }
  4499.  
  4500. for (let j = pageNumber - 1; j >= 1 && numberOfButtonsCreated < this.maxPageNumberButtons; j -= 1) {
  4501. numberOfButtonsCreated += 1;
  4502. this.createPageNumberButton(pageNumber, j, favorites, "afterbegin");
  4503. }
  4504. }
  4505.  
  4506. /**
  4507. * @param {Number} currentPageNumber
  4508. * @param {Number} pageNumber
  4509. * @param {Post[]} favorites
  4510. * @param {InsertPosition} position
  4511. */
  4512. createPageNumberButton(currentPageNumber, pageNumber, favorites, position = "beforeend") {
  4513. const pageNumberButton = document.createElement("button");
  4514. const selected = currentPageNumber === pageNumber;
  4515.  
  4516. pageNumberButton.id = `favorites-page-${pageNumber}`;
  4517. pageNumberButton.title = `Goto page ${pageNumber}`;
  4518. pageNumberButton.className = "pagination-number";
  4519. pageNumberButton.classList.toggle("selected", selected);
  4520. pageNumberButton.onclick = () => {
  4521. this.changePage(pageNumber, favorites);
  4522. };
  4523. this.paginationMenu.insertAdjacentElement(position, pageNumberButton);
  4524. pageNumberButton.textContent = pageNumber;
  4525. }
  4526.  
  4527. /**
  4528. * @param {Post[]} favorites
  4529. */
  4530. updatePageNumberButtonEventListeners(favorites) {
  4531. const pageNumberButtons = document.getElementsByClassName("pagination-number");
  4532.  
  4533. for (const pageNumberButton of pageNumberButtons) {
  4534. const pageNumber = parseInt(Utils.removeNonNumericCharacters(pageNumberButton.id));
  4535.  
  4536. pageNumberButton.onclick = () => {
  4537. this.changePage(pageNumber, favorites);
  4538. };
  4539. }
  4540. }
  4541.  
  4542. /**
  4543. * @param {Post[]} favorites
  4544. */
  4545. createPageTraversalButtons(favorites) {
  4546. const pageCount = this.getPageCount(favorites.length);
  4547. const previousPage = document.createElement("button");
  4548. const firstPage = document.createElement("button");
  4549. const nextPage = document.createElement("button");
  4550. const finalPage = document.createElement("button");
  4551.  
  4552. previousPage.textContent = "<";
  4553. firstPage.textContent = "<<";
  4554. nextPage.textContent = ">";
  4555. finalPage.textContent = ">>";
  4556.  
  4557. previousPage.id = "previous-page";
  4558. firstPage.id = "first-page";
  4559. nextPage.id = "next-page";
  4560. finalPage.id = "final-page";
  4561.  
  4562. previousPage.title = "Goto previous page";
  4563. firstPage.title = "Goto first page";
  4564. nextPage.title = "Goto next page";
  4565. finalPage.title = "Goto last page";
  4566.  
  4567. previousPage.onclick = () => {
  4568. if (this.currentPageNumber - 1 >= 1) {
  4569. this.changePage(this.currentPageNumber - 1, favorites);
  4570. }
  4571. };
  4572. firstPage.onclick = () => {
  4573. this.changePage(1, favorites);
  4574. };
  4575. nextPage.onclick = () => {
  4576. if (this.currentPageNumber + 1 <= pageCount) {
  4577. this.changePage(this.currentPageNumber + 1, favorites);
  4578. }
  4579. };
  4580. finalPage.onclick = () => {
  4581. this.changePage(pageCount, favorites);
  4582. };
  4583. this.paginationMenu.insertAdjacentElement("afterbegin", previousPage);
  4584. this.paginationMenu.insertAdjacentElement("afterbegin", firstPage);
  4585. this.paginationMenu.appendChild(nextPage);
  4586. this.paginationMenu.appendChild(finalPage);
  4587.  
  4588. this.updateVisibilityOfPageTraversalButtons(previousPage, firstPage, nextPage, finalPage, this.getPageCount(favorites.length));
  4589. }
  4590.  
  4591. /**
  4592. * @param {Post[]} favorites
  4593. */
  4594. createGotoSpecificPageInputs(favorites) {
  4595. if (this.firstPageNumberButtonExists() && this.lastPageNumberButtonExists(this.getPageCount(favorites.length))) {
  4596. return;
  4597. }
  4598. const html = `
  4599. <input type="number" placeholder="#" style="width: 4em;" id="goto-page-input">
  4600. <button id="goto-page-button">Go</button>
  4601. `;
  4602. const container = document.createElement("span");
  4603.  
  4604. container.title = "Goto specific page";
  4605. container.innerHTML = html;
  4606. const input = container.children[0];
  4607. const button = container.children[1];
  4608.  
  4609. input.onkeydown = (event) => {
  4610. if (event.key === "Enter") {
  4611. button.click();
  4612. }
  4613. };
  4614. this.paginationMenu.appendChild(container);
  4615. this.updateTraversalButtonEventListeners(favorites);
  4616. }
  4617.  
  4618. /**
  4619. * @param {Post[]} favorites
  4620. */
  4621. updateTraversalButtonEventListeners(favorites) {
  4622. const gotoPageButton = document.getElementById("goto-page-button");
  4623. const finalPageButton = document.getElementById("final-page");
  4624. const input = document.getElementById("goto-page-input");
  4625. const pageCount = this.getPageCount(favorites.length);
  4626.  
  4627. if (gotoPageButton === null || finalPageButton === null || input === null) {
  4628. return;
  4629. }
  4630.  
  4631. gotoPageButton.onclick = () => {
  4632. let pageNumber = parseInt(input.value);
  4633.  
  4634. if (!Utils.isNumber(pageNumber)) {
  4635. return;
  4636. }
  4637. pageNumber = Utils.clamp(pageNumber, 1, pageCount);
  4638. this.changePage(pageNumber, favorites);
  4639.  
  4640. };
  4641. finalPageButton.onclick = () => {
  4642. this.changePage(pageCount, favorites);
  4643. };
  4644. }
  4645.  
  4646. /**
  4647. * @param {Number} pageNumber
  4648. * @param {Post[]} favorites
  4649. */
  4650. showFavorites(pageNumber, favorites) {
  4651. const {start, end} = this.getPaginationRange(pageNumber);
  4652. const newContent = document.createDocumentFragment();
  4653.  
  4654. for (const favorite of favorites.slice(start, end)) {
  4655. favorite.insertAtEndOfContent(newContent);
  4656. }
  4657. this.content.innerHTML = "";
  4658. this.content.appendChild(newContent);
  4659. window.scrollTo(0, Utils.onMobileDevice() ? 10 : 0);
  4660. }
  4661.  
  4662. /**
  4663. * @returns {Boolean}
  4664. */
  4665. firstPageNumberButtonExists() {
  4666. return document.getElementById("favorites-page-1") !== null;
  4667. }
  4668.  
  4669. /**
  4670. * @param {Number} pageCount
  4671. * @returns {Boolean}
  4672. */
  4673. lastPageNumberButtonExists(pageCount) {
  4674. return document.getElementById(`favorites-page-${pageCount}`) !== null;
  4675. }
  4676.  
  4677. /**
  4678. * @param {HTMLButtonElement} previousPage
  4679. * @param {HTMLButtonElement} firstPage
  4680. * @param {HTMLButtonElement} nextPage
  4681. * @param {HTMLButtonElement} finalPage
  4682. * @param {Number} pageCount
  4683. */
  4684. updateVisibilityOfPageTraversalButtons(previousPage, firstPage, nextPage, finalPage, pageCount) {
  4685. const firstNumberExists = this.firstPageNumberButtonExists();
  4686. const lastNumberExists = this.lastPageNumberButtonExists(pageCount);
  4687.  
  4688. if (firstNumberExists && lastNumberExists) {
  4689. previousPage.disabled = true;
  4690. firstPage.disabled = true;
  4691. nextPage.disabled = true;
  4692. finalPage.disabled = true;
  4693. } else {
  4694. if (firstNumberExists) {
  4695. previousPage.disabled = true;
  4696. firstPage.disabled = true;
  4697. }
  4698.  
  4699. if (lastNumberExists) {
  4700. nextPage.disabled = true;
  4701. finalPage.disabled = true;
  4702. }
  4703. }
  4704. }
  4705.  
  4706. /**
  4707. * @param {String} direction
  4708. * @param {Post[]} favorites
  4709. */
  4710. changePageWhileInGallery(direction, favorites) {
  4711. const pageCount = this.getPageCount(favorites.length);
  4712. const onLastPage = this.currentPageNumber === pageCount;
  4713. const onFirstPage = this.currentPageNumber === 1;
  4714. const onlyOnePage = onFirstPage && onLastPage;
  4715.  
  4716. if (onlyOnePage) {
  4717. dispatchEvent(new CustomEvent("didNotChangePageInGallery", {
  4718. detail: direction
  4719. }));
  4720. return;
  4721. }
  4722.  
  4723. if (onLastPage && direction === "ArrowRight") {
  4724. this.changePage(1, favorites);
  4725. return;
  4726. }
  4727.  
  4728. if (onFirstPage && direction === "ArrowLeft") {
  4729. this.changePage(pageCount, favorites);
  4730. return;
  4731. }
  4732. const newPageNumber = direction === "ArrowRight" ? this.currentPageNumber + 1 : this.currentPageNumber - 1;
  4733.  
  4734. this.changePage(newPageNumber, favorites);
  4735. }
  4736.  
  4737. /**
  4738. * @param {Boolean} value
  4739. */
  4740. toggleContentVisibility(value) {
  4741. this.content.style.display = value ? "" : "none";
  4742. }
  4743.  
  4744. /**
  4745. * @param {Post} favorite
  4746. */
  4747. insertNewFavorite(favorite) {
  4748. favorite.insertAtBeginningOfContent(this.content);
  4749. }
  4750.  
  4751. /**
  4752. * @param {Number} id
  4753. * @param {Post[]} favorites
  4754. */
  4755. async findFavorite(id, favorites) {
  4756. const favoriteIds = favorites.map(favorite => favorite.id);
  4757. const index = favoriteIds.indexOf(id);
  4758. const favoriteNotFound = index === -1;
  4759.  
  4760. if (favoriteNotFound) {
  4761. return;
  4762. }
  4763. const pageNumber = Math.floor(index / this.favoritesPerPage) + 1;
  4764.  
  4765. dispatchEvent(new CustomEvent("foundFavorite", {
  4766. detail: id
  4767. }));
  4768.  
  4769. if (this.currentPageNumber !== pageNumber) {
  4770. this.changePage(pageNumber, favorites);
  4771. }
  4772.  
  4773. await Utils.sleep(150);
  4774. Utils.scrollToThumb(id, false, false);
  4775. await Utils.sleep(50);
  4776. Utils.scrollToThumb(id, false, false);
  4777. const thumb = document.getElementById(id);
  4778.  
  4779. if (thumb === null || thumb.classList.contains("blink")) {
  4780. return;
  4781. }
  4782. thumb.classList.add("blink");
  4783. await Utils.sleep(1500);
  4784. thumb.classList.remove("blink");
  4785. }
  4786. }
  4787.  
  4788. class FavoritesSearchFlags {
  4789. /**
  4790. * @type {Boolean}
  4791. */
  4792. searchResultsAreShuffled;
  4793. /**
  4794. * @type {Boolean}
  4795. */
  4796. searchResultsAreInverted;
  4797. /**
  4798. * @type {Boolean}
  4799. */
  4800. searchResultsWereShuffled;
  4801. /**
  4802. * @type {Boolean}
  4803. */
  4804. searchResultsWereInverted;
  4805. /**
  4806. * @type {Boolean}
  4807. */
  4808. recentlyChangedResultsPerPage;
  4809. /**
  4810. * @type {Boolean}
  4811. */
  4812. tagsWereModified;
  4813. /**
  4814. * @type {Boolean}
  4815. */
  4816. excludeBlacklistWasClicked;
  4817. /**
  4818. * @type {Boolean}
  4819. */
  4820. sortingParametersWereChanged;
  4821. /**
  4822. * @type {Boolean}
  4823. */
  4824. allowedRatingsWereChanged;
  4825. /**
  4826. * @type {String}
  4827. */
  4828. searchQuery;
  4829. /**
  4830. * @type {String}
  4831. */
  4832. previousSearchQuery;
  4833.  
  4834. /**
  4835. * @type {Boolean}
  4836. */
  4837. get onFirstPage() {
  4838. const firstPageNumberButton = document.getElementById("favorites-page-1");
  4839. return firstPageNumberButton !== null && firstPageNumberButton.classList.contains("selected");
  4840. }
  4841.  
  4842. /**
  4843. * @type {Boolean}
  4844. */
  4845. get notOnFirstPage() {
  4846. return !this.onFirstPage;
  4847. }
  4848.  
  4849. /**
  4850. * @type {Boolean}
  4851. */
  4852. get aNewSearchCouldProduceDifferentResults() {
  4853. return this.searchQuery !== this.previousSearchQuery ||
  4854. FavoritesLoader.currentState !== FavoritesLoader.states.allFavoritesLoaded ||
  4855. this.searchResultsAreShuffled ||
  4856. this.searchResultsAreInverted ||
  4857. this.searchResultsWereShuffled ||
  4858. this.searchResultsWereInverted ||
  4859. this.recentlyChangedResultsPerPage ||
  4860. this.tagsWereModified ||
  4861. this.excludeBlacklistWasClicked ||
  4862. this.sortingParametersWereChanged ||
  4863. this.allowedRatingsWereChanged ||
  4864. this.notOnFirstPage;
  4865. }
  4866.  
  4867. constructor() {
  4868. this.searchResultsAreShuffled = false;
  4869. this.searchResultsAreInverted = false;
  4870. this.searchResultsWereShuffled = false;
  4871. this.searchResultsWereInverted = false;
  4872. this.recentlyChangedResultsPerPage = false;
  4873. this.tagsWereModified = false;
  4874. this.excludeBlacklistWasClicked = false;
  4875. this.sortingParametersWereChanged = false;
  4876. this.allowedRatingsWereChanged = false;
  4877. this.searchQuery = "";
  4878. this.previousSearchQuery = "";
  4879. }
  4880.  
  4881. resetFlagsImplyingDifferentSearchResults() {
  4882. this.searchResultsWereShuffled = this.searchResultsAreShuffled;
  4883. this.searchResultsWereInverted = this.searchResultsAreInverted;
  4884. this.tagsWereModified = false;
  4885. this.excludeBlacklistWasClicked = false;
  4886. this.sortingParametersWereChanged = false;
  4887. this.allowedRatingsWereChanged = false;
  4888. this.searchResultsAreShuffled = false;
  4889. this.searchResultsAreInverted = false;
  4890. this.recentlyChangedResultsPerPage = false;
  4891. this.previousSearchQuery = this.searchQuery;
  4892. }
  4893. }
  4894.  
  4895. class FavoritesDatabaseWrapper {
  4896. static databaseName = "Favorites";
  4897. static objectStoreName = `user${Utils.getFavoritesPageId()}`;
  4898. static webWorkers = {
  4899. database:
  4900. `
  4901. /* eslint-disable prefer-template */
  4902. /**
  4903. * @param {Number} milliseconds
  4904. * @returns {Promise}
  4905. */
  4906. function sleep(milliseconds) {
  4907. return new Promise(resolve => setTimeout(resolve, milliseconds));
  4908. }
  4909.  
  4910. class FavoritesDatabase {
  4911. /**
  4912. * @type {String}
  4913. */
  4914. name = "Favorites";
  4915. /**
  4916. * @type {String}
  4917. */
  4918. objectStoreName;
  4919. /**
  4920. * @type {Number}
  4921. */
  4922. version;
  4923.  
  4924. /**
  4925. * @param {String} objectStoreName
  4926. * @param {Number | String} version
  4927. */
  4928. constructor(objectStoreName, version) {
  4929. this.objectStoreName = objectStoreName;
  4930. this.version = version;
  4931. }
  4932.  
  4933. createObjectStore() {
  4934. return this.openConnection((event) => {
  4935. /**
  4936. * @type {IDBDatabase}
  4937. */
  4938. const database = event.target.result;
  4939. const objectStore = database
  4940. .createObjectStore(this.objectStoreName, {
  4941. autoIncrement: true
  4942. });
  4943.  
  4944. objectStore.createIndex("id", "id", {
  4945. unique: true
  4946. });
  4947. }).then((event) => {
  4948. event.target.result.close();
  4949. });
  4950. }
  4951.  
  4952. /**
  4953. * @param {Function} onUpgradeNeeded
  4954. * @returns {Promise}
  4955. */
  4956. openConnection(onUpgradeNeeded) {
  4957. return new Promise((resolve, reject) => {
  4958. const request = indexedDB.open(this.name, this.version);
  4959.  
  4960. request.onsuccess = resolve;
  4961. request.onerror = reject;
  4962. request.onupgradeneeded = onUpgradeNeeded;
  4963. });
  4964. }
  4965.  
  4966. /**
  4967. * @param {[{id: String, tags: String, src: String, metadata: String}]} favorites
  4968. */
  4969. storeFavorites(favorites) {
  4970. this.openConnection()
  4971. .then((connectionEvent) => {
  4972. /**
  4973. * @type {IDBDatabase}
  4974. */
  4975. const database = connectionEvent.target.result;
  4976. const transaction = database.transaction(this.objectStoreName, "readwrite");
  4977. const objectStore = transaction.objectStore(this.objectStoreName);
  4978.  
  4979. transaction.oncomplete = () => {
  4980. postMessage({
  4981. response: "finishedStoring"
  4982. });
  4983. database.close();
  4984. };
  4985.  
  4986. transaction.onerror = (event) => {
  4987. console.error(event);
  4988. };
  4989.  
  4990. favorites.forEach(favorite => {
  4991. this.addContentTypeToFavorite(favorite);
  4992. objectStore.put(favorite);
  4993. });
  4994.  
  4995. })
  4996. .catch((event) => {
  4997. const error = event.target.error;
  4998.  
  4999. if (error.name === "VersionError") {
  5000. this.version += 1;
  5001. this.storeFavorites(favorites);
  5002. } else {
  5003. console.error(error);
  5004. }
  5005. });
  5006. }
  5007.  
  5008. /**
  5009. * @param {String[]} idsToDelete
  5010. */
  5011. loadFavorites(idsToDelete) {
  5012. let loadedFavorites = {};
  5013. let database;
  5014.  
  5015. this.openConnection()
  5016. .then(async(connectionEvent) => {
  5017. /**
  5018. * @type {IDBDatabase}
  5019. */
  5020. database = connectionEvent.target.result;
  5021. const transaction = database.transaction(this.objectStoreName, "readwrite");
  5022. const objectStore = transaction.objectStore(this.objectStoreName);
  5023. const index = objectStore.index("id");
  5024.  
  5025. transaction.onerror = (event) => {
  5026. console.error(event);
  5027. };
  5028. transaction.oncomplete = () => {
  5029. postMessage({
  5030. response: "finishedLoading",
  5031. favorites: loadedFavorites
  5032. });
  5033. database.close();
  5034. };
  5035.  
  5036. for (const id of idsToDelete) {
  5037. const deleteRequest = index.getKey(id);
  5038.  
  5039. await new Promise((resolve, reject) => {
  5040. deleteRequest.onsuccess = resolve;
  5041. deleteRequest.onerror = reject;
  5042. }).then((indexEvent) => {
  5043. const primaryKey = indexEvent.target.result;
  5044.  
  5045. if (primaryKey !== undefined) {
  5046. objectStore.delete(primaryKey);
  5047. }
  5048. }).catch((error) => {
  5049. console.error(error);
  5050. });
  5051. }
  5052. const getAllRequest = objectStore.getAll();
  5053.  
  5054. getAllRequest.onsuccess = (event) => {
  5055. loadedFavorites = event.target.result.reverse();
  5056. };
  5057. getAllRequest.onerror = (event) => {
  5058. console.error(event);
  5059. };
  5060. }).catch(async(error) => {
  5061. this.version += 1;
  5062.  
  5063. if (error.name === "NotFoundError") {
  5064. database.close();
  5065. await this.createObjectStore();
  5066. }
  5067. this.loadFavorites(idsToDelete);
  5068. });
  5069. }
  5070.  
  5071. /**
  5072. * @param {[{id: String, tags: String, src: String, metadata: String}]} favorites
  5073. */
  5074. updateFavorites(favorites) {
  5075. this.openConnection()
  5076. .then((event) => {
  5077. /**
  5078. * @type {IDBDatabase}
  5079. */
  5080. const database = event.target.result;
  5081. const favoritesObjectStore = database
  5082. .transaction(this.objectStoreName, "readwrite")
  5083. .objectStore(this.objectStoreName);
  5084. const objectStoreIndex = favoritesObjectStore.index("id");
  5085. let updatedCount = 0;
  5086.  
  5087. favorites.forEach(favorite => {
  5088. const index = objectStoreIndex.getKey(favorite.id);
  5089.  
  5090. this.addContentTypeToFavorite(favorite);
  5091. index.onsuccess = (indexEvent) => {
  5092. const primaryKey = indexEvent.target.result;
  5093.  
  5094. favoritesObjectStore.put(favorite, primaryKey);
  5095. updatedCount += 1;
  5096.  
  5097. if (updatedCount >= favorites.length) {
  5098. database.close();
  5099. }
  5100. };
  5101. });
  5102. })
  5103. .catch((event) => {
  5104. const error = event.target.error;
  5105.  
  5106. if (error.name === "VersionError") {
  5107. this.version += 1;
  5108. this.updateFavorites(favorites);
  5109. } else {
  5110. console.error(error);
  5111. }
  5112. });
  5113. }
  5114.  
  5115. /**
  5116. * @param {{id: String, tags: String, src: String, metadata: String}} favorite
  5117. */
  5118. addContentTypeToFavorite(favorite) {
  5119. const tags = favorite.tags + " ";
  5120. const isAnimated = tags.includes("animated ") || tags.includes("video ");
  5121. const isGif = isAnimated && !tags.includes("video ");
  5122.  
  5123. favorite.type = isGif ? "gif" : isAnimated ? "video" : "image";
  5124. }
  5125. }
  5126.  
  5127. /**
  5128. * @type {FavoritesDatabase}
  5129. */
  5130. const favoritesDatabase = new FavoritesDatabase(null, 1);
  5131.  
  5132. onmessage = (message) => {
  5133. const request = message.data;
  5134.  
  5135. switch (request.command) {
  5136. case "create":
  5137. favoritesDatabase.objectStoreName = request.objectStoreName;
  5138. favoritesDatabase.version = request.version;
  5139. break;
  5140.  
  5141. case "store":
  5142. favoritesDatabase.storeFavorites(request.favorites);
  5143. break;
  5144.  
  5145. case "load":
  5146. favoritesDatabase.loadFavorites(request.idsToDelete);
  5147. break;
  5148.  
  5149. case "update":
  5150. favoritesDatabase.updateFavorites(request.favorites);
  5151. break;
  5152.  
  5153. default:
  5154. break;
  5155. }
  5156. };
  5157.  
  5158. `
  5159. };
  5160.  
  5161. /**
  5162. * @type {Function}
  5163. */
  5164. onFavoritesStored;
  5165. /**
  5166. * @type {Function}
  5167. */
  5168. onFavoritesLoaded;
  5169. /**
  5170. * @type {Worker}
  5171. */
  5172. databaseWorker;
  5173. /**
  5174. * @type {String[]}
  5175. */
  5176. favoriteIdsRequiringMetadataDatabaseUpdate;
  5177. /**
  5178. * @type {Number}
  5179. */
  5180. newMetadataReceivedTimeout;
  5181.  
  5182. /**
  5183. * @param {Function} onFavoritesStored
  5184. * @param {Function} onFavoritesLoaded
  5185. */
  5186. constructor(onFavoritesStored, onFavoritesLoaded) {
  5187. this.onFavoritesStored = onFavoritesStored;
  5188. this.onFavoritesLoaded = onFavoritesLoaded;
  5189. this.favoriteIdsRequiringMetadataDatabaseUpdate = [];
  5190. this.addEventListeners();
  5191. this.initializeDatabase();
  5192. }
  5193.  
  5194. addEventListeners() {
  5195. window.addEventListener("missingMetadata", (event) => {
  5196. this.addNewMetadata(event.detail);
  5197. });
  5198. }
  5199.  
  5200. initializeDatabase() {
  5201. this.databaseWorker = new Worker(Utils.getWorkerURL(FavoritesDatabaseWrapper.webWorkers.database));
  5202. this.databaseWorker.onmessage = (message) => {
  5203. switch (message.data.response) {
  5204. case "finishedLoading":
  5205. this.onFavoritesLoaded(message.data.favorites);
  5206. break;
  5207.  
  5208. case "finishedStoring":
  5209. this.onFavoritesStored();
  5210. break;
  5211.  
  5212. default:
  5213. break;
  5214. }
  5215. };
  5216. this.databaseWorker.postMessage({
  5217. command: "create",
  5218. objectStoreName: FavoritesDatabaseWrapper.objectStoreName,
  5219. version: 1
  5220. });
  5221. }
  5222.  
  5223. /**
  5224. * @returns {String[]}
  5225. */
  5226. getIdsToDeleteOnReload() {
  5227. if (Utils.userIsOnTheirOwnFavoritesPage()) {
  5228. const idsToDelete = Utils.getIdsToDeleteOnReload();
  5229.  
  5230. Utils.clearIdsToDeleteOnReload();
  5231. return idsToDelete;
  5232. }
  5233. return [];
  5234. }
  5235.  
  5236. /**
  5237. * @param {Post[]} favorites
  5238. */
  5239. storeAllFavorites(favorites) {
  5240. this.storeFavorites(favorites.slice().reverse());
  5241. }
  5242.  
  5243. /**
  5244. * @param {Post[]} favorites
  5245. */
  5246. async storeFavorites(favorites) {
  5247. await Utils.sleep(500);
  5248.  
  5249. this.databaseWorker.postMessage({
  5250. command: "store",
  5251. favorites: favorites.map(post => post.databaseRecord)
  5252. });
  5253. }
  5254.  
  5255. loadAllFavorites() {
  5256. this.databaseWorker.postMessage({
  5257. command: "load",
  5258. idsToDelete: this.getIdsToDeleteOnReload()
  5259. });
  5260. }
  5261.  
  5262. /**
  5263. * @param {String} postId
  5264. */
  5265. addNewMetadata(postId) {
  5266. if (!Post.allPosts.has(postId)) {
  5267. return;
  5268. }
  5269. const batchSize = 500;
  5270. const waitTime = 1000;
  5271.  
  5272. clearTimeout(this.newMetadataReceivedTimeout);
  5273. this.favoriteIdsRequiringMetadataDatabaseUpdate.push(postId);
  5274.  
  5275. if (this.favoriteIdsRequiringMetadataDatabaseUpdate.length >= batchSize) {
  5276. this.updateMetadataInDatabase();
  5277. return;
  5278. }
  5279. this.newMetadataReceivedTimeout = setTimeout(() => {
  5280. this.updateMetadataInDatabase();
  5281. }, waitTime);
  5282. }
  5283.  
  5284. updateMetadataInDatabase() {
  5285. this.updateFavorites(this.favoriteIdsRequiringMetadataDatabaseUpdate.map(id => Post.allPosts.get(id)));
  5286. this.favoriteIdsRequiringMetadataDatabaseUpdate = [];
  5287. }
  5288.  
  5289. /**
  5290. * @param {Post[]} posts
  5291. */
  5292. updateFavorites(posts) {
  5293. this.databaseWorker.postMessage({
  5294. command: "update",
  5295. favorites: posts.map(post => post.databaseRecord)
  5296. });
  5297. }
  5298. }
  5299.  
  5300. class FavoritesLoader {
  5301. static states = {
  5302. initial: 0,
  5303. fetchingFavorites: 1,
  5304. loadingFavoritesFromDatabase: 2,
  5305. allFavoritesLoaded: 3
  5306. };
  5307. static currentState = FavoritesLoader.states.initial;
  5308. static tagNegation = {
  5309. useTagBlacklist: true,
  5310. negatedTagBlacklist: Utils.negateTags(Utils.tagBlacklist)
  5311. };
  5312.  
  5313. static get disabled() {
  5314. return !Utils.onFavoritesPage();
  5315. }
  5316.  
  5317. /**
  5318. * @type {Post[]}
  5319. */
  5320. allFavorites;
  5321. /**
  5322. * @type {Post[]}
  5323. */
  5324. latestSearchResults;
  5325. /**
  5326. * @type {HTMLLabelElement}
  5327. */
  5328. matchCountLabel;
  5329. /**
  5330. * @type {Number}
  5331. */
  5332. searchResultCount;
  5333. /**
  5334. * @type {Number | null}
  5335. */
  5336. expectedTotalFavoritesCount;
  5337. /**
  5338. * @type {String}
  5339. */
  5340. searchQuery;
  5341. /**
  5342. * @type {Post[]}
  5343. */
  5344. searchResultsWhileFetching;
  5345. /**
  5346. * @type {Number}
  5347. */
  5348. allowedRatings;
  5349. /**
  5350. * @type {FavoritesFetcher}
  5351. */
  5352. fetcher;
  5353. /**
  5354. * @type {FetchedFavoritesQueue}
  5355. */
  5356. fetchedQueue;
  5357. /**
  5358. * @type {FavoritesPaginator}
  5359. */
  5360. paginator;
  5361. /**
  5362. * @type {FavoritesSearchFlags}
  5363. */
  5364. searchFlags;
  5365. /**
  5366. * @type {FavoritesDatabaseWrapper}
  5367. */
  5368. database;
  5369.  
  5370. /**
  5371. * @type {String}
  5372. */
  5373. get finalSearchQuery() {
  5374. if (FavoritesLoader.tagNegation.useTagBlacklist) {
  5375. return `${this.searchQuery} ${FavoritesLoader.tagNegation.negatedTagBlacklist}`;
  5376. }
  5377. return this.searchQuery;
  5378. }
  5379.  
  5380. /**
  5381. * @type {Boolean}
  5382. */
  5383. get matchCountLabelExists() {
  5384. if (this.matchCountLabel === null || !document.contains(this.matchCountLabel)) {
  5385. this.matchCountLabel = document.getElementById("match-count-label");
  5386.  
  5387. if (this.matchCountLabel === null) {
  5388. return false;
  5389. }
  5390. }
  5391. return true;
  5392. }
  5393.  
  5394. /**
  5395. * @type {Set.<String>}
  5396. */
  5397. get allFavoriteIds() {
  5398. return new Set(Array.from(this.allFavorites.values()).map(post => post.id));
  5399. }
  5400.  
  5401. /**
  5402. * @type {Post[]}
  5403. */
  5404. get getFavoritesMatchedByLastSearch() {
  5405. return this.allFavorites.filter(post => post.matchedByMostRecentSearch);
  5406. }
  5407.  
  5408. constructor() {
  5409. if (FavoritesLoader.disabled) {
  5410. return;
  5411. }
  5412. this.initializeFields();
  5413. this.initializeComponents();
  5414. this.addEventListeners();
  5415. this.setExpectedFavoritesCount();
  5416. Utils.clearOriginalFavoritesPage();
  5417. this.searchFavorites();
  5418. }
  5419.  
  5420. initializeFields() {
  5421. this.allFavorites = [];
  5422. this.latestSearchResults = [];
  5423. this.searchResultsWhileFetching = [];
  5424. this.matchCountLabel = document.getElementById("match-count-label");
  5425. this.allowedRatings = Utils.loadAllowedRatings();
  5426. this.expectedTotalFavoritesCount = null;
  5427. this.searchResultCount = 0;
  5428. this.searchQuery = "";
  5429. }
  5430.  
  5431. initializeComponents() {
  5432. this.fetchedQueue = new FetchedFavoritesQueue((request) => {
  5433. this.processFetchedFavorites(request.fetchedFavorites);
  5434. });
  5435. this.fetcher = new FavoritesFetcher(() => {
  5436. this.onAllFavoritesFetched();
  5437. }, (request) => {
  5438. this.fetchedQueue.enqueue(request);
  5439. });
  5440. this.paginator = new FavoritesPaginator();
  5441. this.searchFlags = new FavoritesSearchFlags();
  5442. this.database = new FavoritesDatabaseWrapper(() => {
  5443. this.onFavoritesStoredToDatabase();
  5444. }, (favorites) => {
  5445. this.onAllFavoritesLoadedFromDatabase(favorites);
  5446. });
  5447. }
  5448.  
  5449. addEventListeners() {
  5450. window.addEventListener("modifiedTags", () => {
  5451. this.searchFlags.tagsWereModified = true;
  5452. });
  5453. window.addEventListener("reachedEndOfGallery", (event) => {
  5454. this.paginator.changePageWhileInGallery(event.detail, this.latestSearchResults);
  5455. });
  5456. }
  5457.  
  5458. setExpectedFavoritesCount() {
  5459. const profileURL = `https://rule34.xxx/index.php?page=account&s=profile&id=${Utils.getFavoritesPageId()}`;
  5460.  
  5461. fetch(profileURL)
  5462. .then((response) => {
  5463. if (response.ok) {
  5464. return response.text();
  5465. }
  5466. throw new Error(response.status);
  5467. })
  5468. .then((html) => {
  5469. const favoritesURL = Array.from(new DOMParser().parseFromString(html, "text/html").querySelectorAll("a"))
  5470. .find(a => a.href.includes("page=favorites&s=view"));
  5471. const favoritesCount = parseInt(favoritesURL.textContent);
  5472.  
  5473. this.expectedTotalFavoritesCount = Math.max(favoritesCount - 2, 0);
  5474. })
  5475. .catch(() => {
  5476. console.error(`Could not find total favorites count from ${profileURL}, are you logged in?`);
  5477. });
  5478. }
  5479.  
  5480. /**
  5481. * @param {String} searchQuery
  5482. */
  5483. searchFavorites(searchQuery) {
  5484. this.setSearchQuery(searchQuery);
  5485. dispatchEvent(new Event("searchStarted"));
  5486. this.showSearchResults();
  5487. }
  5488.  
  5489. /**
  5490. * @param {String} searchQuery
  5491. */
  5492. setSearchQuery(searchQuery) {
  5493. if (searchQuery !== undefined) {
  5494. this.searchQuery = searchQuery;
  5495. this.searchFlags.searchQuery = searchQuery;
  5496. }
  5497. }
  5498.  
  5499. showSearchResults() {
  5500. switch (FavoritesLoader.currentState) {
  5501. case FavoritesLoader.states.initial:
  5502. this.loadAllFavoritesFromDatabase();
  5503. break;
  5504.  
  5505. case FavoritesLoader.states.fetchingFavorites:
  5506. this.showSearchResultsWhileFetchingFavorites();
  5507. break;
  5508.  
  5509. case FavoritesLoader.states.loadingFavoritesFromDatabase:
  5510. break;
  5511.  
  5512. case FavoritesLoader.states.allFavoritesLoaded:
  5513. this.showSearchResultsAfterAllFavoritesLoaded();
  5514. break;
  5515.  
  5516. default:
  5517. console.error(`Invalid FavoritesLoader state: ${FavoritesLoader.currentState}`);
  5518. break;
  5519. }
  5520. }
  5521.  
  5522. showSearchResultsWhileFetchingFavorites() {
  5523. this.searchResultsWhileFetching = this.getSearchResults(this.allFavorites);
  5524. this.paginateSearchResults(this.searchResultsWhileFetching);
  5525. }
  5526.  
  5527. showSearchResultsAfterAllFavoritesLoaded() {
  5528. this.paginateSearchResults(this.getSearchResults(this.allFavorites));
  5529. }
  5530.  
  5531. /**
  5532. * @param {Post[]} posts
  5533. * @returns {Post[]}
  5534. */
  5535. getSearchResults(posts) {
  5536. const searchCommand = new SearchCommand(this.finalSearchQuery);
  5537. const results = [];
  5538.  
  5539. for (const post of posts) {
  5540. if (searchCommand.matches(post)) {
  5541. results.push(post);
  5542. post.setMatched(true);
  5543. } else {
  5544. post.setMatched(false);
  5545. }
  5546. }
  5547. return results;
  5548. }
  5549.  
  5550. fetchNewFavoritesOnReload() {
  5551. this.fetcher.onAllRequestsCompleted = (newFavorites) => {
  5552. this.addNewFavoritesOnReload(newFavorites);
  5553. };
  5554. this.fetcher.fetchAllNewFavoritesOnReload(this.allFavoriteIds);
  5555. }
  5556.  
  5557. /**
  5558. * @param {Post[]} newFavorites
  5559. */
  5560. addNewFavoritesOnReload(newFavorites) {
  5561. this.allFavorites = newFavorites.concat(this.allFavorites);
  5562. this.latestSearchResults = newFavorites.concat(this.latestSearchResults);
  5563.  
  5564. if (newFavorites.length === 0) {
  5565. dispatchEvent(new CustomEvent("newFavoritesFetchedOnReload", {
  5566. detail: {
  5567. empty: true,
  5568. thumbs: []
  5569. }
  5570. }));
  5571. this.toggleStatusText(false);
  5572. return;
  5573. }
  5574. this.setStatusText(`Found ${newFavorites.length} new favorite${newFavorites.length === 1 ? "" : "s"}`);
  5575. this.toggleStatusText(false, 1000);
  5576. this.database.storeFavorites(newFavorites);
  5577. this.insertNewFavorites(newFavorites);
  5578. }
  5579.  
  5580. fetchAllFavorites() {
  5581. FavoritesLoader.currentState = FavoritesLoader.states.fetchingFavorites;
  5582. this.paginator.toggleContentVisibility(true);
  5583. this.paginator.insertPaginationMenuContainer();
  5584. this.paginator.createPaginationMenu(1, []);
  5585. this.fetcher.fetchAllFavorites();
  5586. dispatchEvent(new Event("readyToSearch"));
  5587. setTimeout(() => {
  5588. dispatchEvent(new Event("startedFetchingFavorites"));
  5589. }, 50);
  5590. }
  5591.  
  5592. updateStatusWhileFetching() {
  5593. const prefix = Utils.onMobileDevice() ? "" : "Favorites ";
  5594. let statusText = `Fetching ${prefix}${this.allFavorites.length}`;
  5595.  
  5596. if (this.expectedTotalFavoritesCount !== null) {
  5597. statusText = `${statusText} / ${this.expectedTotalFavoritesCount}`;
  5598. }
  5599. this.setStatusText(statusText);
  5600. }
  5601.  
  5602. /**
  5603. * @param {Post[]} favorites
  5604. */
  5605. processFetchedFavorites(favorites) {
  5606. const matchedFavorites = this.getSearchResults(favorites);
  5607.  
  5608. this.searchResultsWhileFetching = this.searchResultsWhileFetching.concat(matchedFavorites);
  5609. const searchResultsWhileFetchingWithAllowedRatings = this.getResultsWithAllowedRatings(this.searchResultsWhileFetching);
  5610.  
  5611. this.updateMatchCount(searchResultsWhileFetchingWithAllowedRatings.length);
  5612. this.allFavorites = this.allFavorites.concat(favorites);
  5613. this.addFetchedFavoritesToContent(searchResultsWhileFetchingWithAllowedRatings);
  5614. this.updateStatusWhileFetching();
  5615. dispatchEvent(new CustomEvent("favoritesFetched", {
  5616. detail: favorites.map(post => post.root)
  5617. }));
  5618. }
  5619.  
  5620. invertSearchResults() {
  5621. this.resetMatchCount();
  5622. this.allFavorites.forEach((post) => {
  5623. post.toggleMatched();
  5624. });
  5625. const invertedSearchResults = this.getFavoritesMatchedByLastSearch;
  5626.  
  5627. this.searchFlags.searchResultsAreInverted = true;
  5628. this.paginateSearchResults(invertedSearchResults);
  5629. window.scrollTo(0, 0);
  5630. }
  5631.  
  5632. shuffleSearchResults() {
  5633. const matchedPosts = this.getFavoritesMatchedByLastSearch;
  5634.  
  5635. Utils.shuffleArray(matchedPosts);
  5636. this.searchFlags.searchResultsAreShuffled = true;
  5637. this.paginateSearchResults(matchedPosts);
  5638. }
  5639.  
  5640. onAllFavoritesFetched() {
  5641. this.latestSearchResults = this.getResultsWithAllowedRatings(this.searchResultsWhileFetching);
  5642. dispatchEvent(new CustomEvent("newSearchResults", {
  5643. detail: this.latestSearchResults
  5644. }));
  5645. this.onAllFavoritesLoaded();
  5646. this.database.storeAllFavorites(this.allFavorites);
  5647. this.setStatusText("Saving favorites");
  5648. }
  5649.  
  5650. /**
  5651. * @param {Object[]} records
  5652. */
  5653. onAllFavoritesLoadedFromDatabase(records) {
  5654. this.toggleLoadingUI(false);
  5655.  
  5656. if (records.length === 0) {
  5657. this.fetchAllFavorites();
  5658. return;
  5659. }
  5660. this.setStatusText("All favorites loaded");
  5661. this.paginateSearchResults(this.deserializeFavorites(records));
  5662. dispatchEvent(new Event("favoritesLoadedFromDatabase"));
  5663. this.onAllFavoritesLoaded();
  5664. setTimeout(() => {
  5665. this.fetchNewFavoritesOnReload();
  5666. }, 100);
  5667. }
  5668.  
  5669. onFavoritesStoredToDatabase() {
  5670. this.setStatusText("All favorites saved");
  5671. this.toggleStatusText(false, 1000);
  5672. }
  5673.  
  5674. onAllFavoritesLoaded() {
  5675. dispatchEvent(new Event("readyToSearch"));
  5676. dispatchEvent(new Event("favoritesLoaded"));
  5677. FavoritesLoader.currentState = FavoritesLoader.states.allFavoritesLoaded;
  5678. }
  5679.  
  5680. /**
  5681. * @param {Boolean} value
  5682. */
  5683. toggleLoadingUI(value) {
  5684. this.showLoadingWheel(value);
  5685. this.paginator.toggleContentVisibility(!value);
  5686. }
  5687.  
  5688. /**
  5689. * @param {Object[]} records
  5690. * @returns {Post[]}}
  5691. */
  5692. deserializeFavorites(records) {
  5693. const searchCommand = new SearchCommand(this.finalSearchQuery);
  5694. const searchResults = [];
  5695.  
  5696. for (const record of records) {
  5697. const post = new Post(record, true);
  5698. const isBlacklisted = !searchCommand.matches(post);
  5699.  
  5700. if (isBlacklisted) {
  5701. if (!Utils.userIsOnTheirOwnFavoritesPage()) {
  5702. continue;
  5703. }
  5704. post.setMatched(false);
  5705. } else {
  5706. searchResults.push(post);
  5707. }
  5708. this.allFavorites.push(post);
  5709. }
  5710. return searchResults;
  5711. }
  5712.  
  5713. loadAllFavoritesFromDatabase() {
  5714. FavoritesLoader.currentState = FavoritesLoader.states.loadingFavoritesFromDatabase;
  5715. this.toggleLoadingUI(true);
  5716. this.setStatusText("Loading favorites");
  5717. this.database.loadAllFavorites();
  5718. }
  5719.  
  5720. /**
  5721. * @param {Boolean} value
  5722. */
  5723. showLoadingWheel(value) {
  5724. document.getElementById("loading-wheel").style.display = value ? "flex" : "none";
  5725. }
  5726.  
  5727. /**
  5728. * @param {Boolean} value
  5729. * @param {Number} delay
  5730. */
  5731. async toggleStatusText(value, delay) {
  5732. if (delay !== undefined && delay > 0) {
  5733. await Utils.sleep(delay);
  5734. }
  5735. document.getElementById("favorites-load-status-label").style.display = value ? "inline-block" : "none";
  5736. }
  5737.  
  5738. /**
  5739. * @param {String} text
  5740. * @param {Number} delay
  5741. */
  5742. async setStatusText(text, delay) {
  5743. if (delay !== undefined && delay > 0) {
  5744. await Utils.sleep(delay);
  5745. }
  5746. document.getElementById("favorites-load-status-label").textContent = text;
  5747. }
  5748.  
  5749. resetMatchCount() {
  5750. this.updateMatchCount(0);
  5751. }
  5752.  
  5753. /**
  5754. * @param {Number} value
  5755. */
  5756. updateMatchCount(value) {
  5757. if (!this.matchCountLabelExists) {
  5758. return;
  5759. }
  5760. this.searchResultCount = value === undefined ? this.getSearchResults(this.allFavorites).length : value;
  5761. const suffix = this.searchResultCount === 1 ? "Match" : "Matches";
  5762.  
  5763. this.matchCountLabel.textContent = `${this.searchResultCount} ${suffix}`;
  5764. }
  5765.  
  5766. /**
  5767. * @param {Number} value
  5768. */
  5769. incrementMatchCount(value) {
  5770. if (!this.matchCountLabelExists) {
  5771. return;
  5772. }
  5773. this.searchResultCount += value === undefined ? 1 : value;
  5774. this.matchCountLabel.textContent = `${this.searchResultCount} Matches`;
  5775. }
  5776.  
  5777. /**
  5778. * @param {Post[]} newPosts
  5779. */
  5780. async insertNewFavorites(newPosts) {
  5781. const searchCommand = new SearchCommand(this.finalSearchQuery);
  5782. const insertedPosts = [];
  5783. const metadataPopulateWaitTime = 1000;
  5784.  
  5785. newPosts.reverse();
  5786.  
  5787. if (this.allowedRatings !== 7) {
  5788. await Utils.sleep(metadataPopulateWaitTime);
  5789. }
  5790.  
  5791. for (const post of newPosts) {
  5792. if (this.matchesSearchAndRating(searchCommand, post)) {
  5793. this.paginator.insertNewFavorite(post);
  5794. insertedPosts.push(post);
  5795. }
  5796. }
  5797. this.paginator.createPaginationMenu(this.paginator.currentPageNumber, this.getFavoritesMatchedByLastSearch);
  5798. setTimeout(() => {
  5799. dispatchEvent(new CustomEvent("newFavoritesFetchedOnReload", {
  5800. detail: {
  5801. empty: false,
  5802. thumbs: insertedPosts.map(post => post.root)
  5803. }
  5804. }));
  5805. }, 250);
  5806. dispatchEvent(new CustomEvent("newSearchResults", {
  5807. detail: this.latestSearchResults
  5808. }));
  5809. }
  5810.  
  5811. /**
  5812. * @param {Post[]} favorites
  5813. */
  5814. addFetchedFavoritesToContent(favorites) {
  5815. this.paginator.paginateWhileFetching(favorites);
  5816. }
  5817.  
  5818. /**
  5819. * @param {Post[]} searchResults
  5820. */
  5821. paginateSearchResults(searchResults) {
  5822. if (!this.searchFlags.aNewSearchCouldProduceDifferentResults) {
  5823. return;
  5824. }
  5825. searchResults = this.sortPosts(searchResults);
  5826. searchResults = this.getResultsWithAllowedRatings(searchResults);
  5827. this.latestSearchResults = searchResults;
  5828. this.updateMatchCount(searchResults.length);
  5829. this.paginator.paginate(searchResults);
  5830. this.searchFlags.resetFlagsImplyingDifferentSearchResults();
  5831. dispatchEvent(new CustomEvent("newSearchResults", {
  5832. detail: searchResults
  5833. }));
  5834. }
  5835.  
  5836. /**
  5837. * @param {Boolean} value
  5838. */
  5839. toggleTagBlacklistExclusion(value) {
  5840. FavoritesLoader.tagNegation.useTagBlacklist = value;
  5841. this.searchFlags.excludeBlacklistWasClicked = true;
  5842. }
  5843.  
  5844. /**
  5845. * @param {Number} value
  5846. */
  5847. updateResultsPerPage(value) {
  5848. this.paginator.maxFavoritesPerPage = value;
  5849. this.searchFlags.recentlyChangedResultsPerPage = true;
  5850. this.searchFavorites();
  5851. }
  5852.  
  5853. /**
  5854. * @param {Post[]} posts
  5855. * @returns {Post[]}
  5856. */
  5857. sortPosts(posts) {
  5858. if (this.searchFlags.searchResultsAreShuffled) {
  5859. return posts;
  5860. }
  5861. const sortedPosts = posts.slice();
  5862. const sortingMethod = Utils.getSortingMethod();
  5863.  
  5864. if (sortingMethod === "random") {
  5865. return Utils.shuffleArray(sortedPosts);
  5866. }
  5867.  
  5868. if (sortingMethod !== "default") {
  5869. sortedPosts.sort((b, a) => {
  5870. switch (sortingMethod) {
  5871. case "score":
  5872. return a.metadata.score - b.metadata.score;
  5873.  
  5874. case "width":
  5875. return a.metadata.width - b.metadata.width;
  5876.  
  5877. case "height":
  5878. return a.metadata.height - b.metadata.height;
  5879.  
  5880. case "create":
  5881. return a.metadata.creationTimestamp - b.metadata.creationTimestamp;
  5882.  
  5883. case "change":
  5884. return a.metadata.lastChangedTimestamp - b.metadata.lastChangedTimestamp;
  5885.  
  5886. case "id":
  5887. return a.metadata.id - b.metadata.id;
  5888.  
  5889. default:
  5890. return 0;
  5891. }
  5892. });
  5893. }
  5894.  
  5895. if (this.sortAscending()) {
  5896. sortedPosts.reverse();
  5897. }
  5898. return sortedPosts;
  5899. }
  5900.  
  5901. /**
  5902. * @returns {Boolean}
  5903. */
  5904. sortAscending() {
  5905. const sortFavoritesAscending = document.getElementById("sort-ascending");
  5906. return sortFavoritesAscending === null ? false : sortFavoritesAscending.checked;
  5907. }
  5908.  
  5909. onSortingParametersChanged() {
  5910. this.searchFlags.sortingParametersWereChanged = true;
  5911. const matchedPosts = this.getFavoritesMatchedByLastSearch;
  5912.  
  5913. this.paginateSearchResults(matchedPosts);
  5914. dispatchEvent(new Event("sortingParametersChanged"));
  5915. }
  5916.  
  5917. /**
  5918. * @param {Number} allowedRatings
  5919. */
  5920. onAllowedRatingsChanged(allowedRatings) {
  5921. this.allowedRatings = allowedRatings;
  5922. this.searchFlags.allowedRatingsWereChanged = true;
  5923. const matchedPosts = this.getFavoritesMatchedByLastSearch;
  5924.  
  5925. this.paginateSearchResults(matchedPosts);
  5926. }
  5927.  
  5928. /**
  5929. * @returns {Boolean}
  5930. */
  5931. allRatingsAreAllowed() {
  5932. return this.allowedRatings === 7;
  5933. }
  5934.  
  5935. /**
  5936. * @param {Post} post
  5937. * @returns {Boolean}
  5938. */
  5939. ratingIsAllowed(post) {
  5940. if (this.allRatingsAreAllowed()) {
  5941. return true;
  5942. }
  5943. // eslint-disable-next-line no-bitwise
  5944. return (post.metadata.rating & this.allowedRatings) > 0;
  5945. }
  5946.  
  5947. /**
  5948. * @param {Post[]} searchResults
  5949. * @returns {Post[]}
  5950. */
  5951. getResultsWithAllowedRatings(searchResults) {
  5952. if (this.allRatingsAreAllowed()) {
  5953. return searchResults;
  5954. }
  5955. return searchResults.filter(post => this.ratingIsAllowed(post));
  5956. }
  5957.  
  5958. /**
  5959. * @param {SearchCommand} searchCommand
  5960. * @param {Post} post
  5961. * @returns {Boolean}
  5962. */
  5963. matchesSearchAndRating(searchCommand, post) {
  5964. return this.ratingIsAllowed(post) && searchCommand.matches(post);
  5965. }
  5966.  
  5967. /**
  5968. * @param {String} id
  5969. */
  5970. findFavorite(id) {
  5971. this.paginator.findFavorite(id, this.latestSearchResults);
  5972. }
  5973. }
  5974.  
  5975. class FavoritesMenu {
  5976. static uiHTML = `
  5977. <div id="favorites-search-gallery-menu" class="light-green-gradient not-highlightable">
  5978. <style>
  5979. #favorites-search-gallery-menu {
  5980. position: sticky;
  5981. top: 0;
  5982. padding: 10px;
  5983. z-index: 30;
  5984. margin-bottom: 10px;
  5985.  
  5986. input::-webkit-outer-spin-button,
  5987. input::-webkit-inner-spin-button {
  5988. -webkit-appearance: none;
  5989. appearance: none;
  5990. margin: 0;
  5991. }
  5992. }
  5993.  
  5994. #favorites-search-gallery-menu-panels {
  5995. >div {
  5996. flex: 1;
  5997. }
  5998. }
  5999.  
  6000. #left-favorites-panel {
  6001. flex: 10 !important;
  6002.  
  6003. >div:first-of-type {
  6004. margin-bottom: 5px;
  6005.  
  6006. >label {
  6007. align-content: center;
  6008. margin-right: 5px;
  6009. margin-top: 4px;
  6010. }
  6011.  
  6012. >button {
  6013. height: 35px;
  6014. border: none;
  6015. border-radius: 4px;
  6016.  
  6017. &:hover {
  6018. filter: brightness(140%);
  6019. }
  6020. }
  6021.  
  6022. >button[disabled] {
  6023. filter: none !important;
  6024. cursor: wait !important;
  6025. }
  6026. }
  6027. }
  6028.  
  6029. #right-favorites-panel {
  6030. flex: 9 !important;
  6031. margin-left: 30px;
  6032. display: none;
  6033. }
  6034.  
  6035. textarea {
  6036. max-width: 100%;
  6037. height: 50px;
  6038. width: 99%;
  6039. padding: 10px;
  6040. border-radius: 6px;
  6041. resize: vertical;
  6042. }
  6043.  
  6044. button,
  6045. input[type="checkbox"] {
  6046. cursor: pointer;
  6047. }
  6048.  
  6049. .checkbox {
  6050. display: block;
  6051. padding: 2px 6px 2px 0px;
  6052. border-radius: 4px;
  6053. margin-left: -3px;
  6054. height: 27px;
  6055.  
  6056. >input {
  6057. vertical-align: -5px;
  6058. }
  6059. }
  6060.  
  6061. .loading-wheel {
  6062. border: 16px solid #f3f3f3;
  6063. border-top: 16px solid #3498db;
  6064. border-radius: 50%;
  6065. width: 120px;
  6066. height: 120px;
  6067. animation: spin 1s ease-in-out infinite;
  6068. pointer-events: none;
  6069. z-index: 9990;
  6070. position: fixed;
  6071. max-height: 100vh;
  6072. margin: 0;
  6073. top: 50%;
  6074. left: 50%;
  6075. transform: translate(-50%, -50%);
  6076. }
  6077.  
  6078. @keyframes spin {
  6079. 0% {
  6080. transform: rotate(0deg);
  6081. }
  6082.  
  6083. 100% {
  6084. transform: rotate(360deg);
  6085. }
  6086. }
  6087.  
  6088. .add-or-remove-button {
  6089. position: absolute;
  6090. left: 0;
  6091. top: 0;
  6092. width: 40%;
  6093. font-weight: bold;
  6094. background: none;
  6095. border: none;
  6096. z-index: 2;
  6097. filter: grayscale(70%);
  6098.  
  6099. &:active,
  6100. &:hover {
  6101. filter: none !important;
  6102. }
  6103. }
  6104.  
  6105. .remove-favorite-button {
  6106. color: red;
  6107. }
  6108.  
  6109. .add-favorite-button {
  6110. >svg {
  6111. fill: hotpink;
  6112. }
  6113. }
  6114.  
  6115. .statistic-hint {
  6116. position: absolute;
  6117. z-index: 3;
  6118. text-align: center;
  6119. right: 0;
  6120. top: 0;
  6121. background: white;
  6122. color: #0075FF;
  6123. font-weight: bold;
  6124. /* font-size: 18px; */
  6125. pointer-events: none;
  6126. font-size: calc(8px + (20 - 8) * ((100vw - 300px) / (3840 - 300)));
  6127. width: 55%;
  6128. padding: 2px 0px;
  6129. border-bottom-left-radius: 4px;
  6130. }
  6131.  
  6132. img {
  6133. -webkit-user-drag: none;
  6134. -khtml-user-drag: none;
  6135. -moz-user-drag: none;
  6136. -o-user-drag: none;
  6137. }
  6138.  
  6139. .favorite {
  6140. position: relative;
  6141. -webkit-touch-callout: none;
  6142. -webkit-user-select: none;
  6143. -khtml-user-select: none;
  6144. -moz-user-select: none;
  6145. -ms-user-select: none;
  6146. user-select: none;
  6147.  
  6148. >a,
  6149. >div {
  6150. display: block;
  6151. overflow: hidden;
  6152. position: relative;
  6153. cursor: default;
  6154.  
  6155. >img:first-child {
  6156. width: 100%;
  6157. z-index: 1;
  6158. }
  6159.  
  6160. >a>div {
  6161. height: 100%;
  6162. }
  6163.  
  6164. >canvas {
  6165. width: 100%;
  6166. position: absolute;
  6167. top: 0;
  6168. left: 0;
  6169. pointer-events: none;
  6170. z-index: 1;
  6171. }
  6172. }
  6173.  
  6174. &.hidden {
  6175. display: none;
  6176. }
  6177. }
  6178.  
  6179. .found {
  6180. opacity: 1;
  6181. animation: wiggle 2s;
  6182. }
  6183.  
  6184. @keyframes wiggle {
  6185.  
  6186. 10%,
  6187. 90% {
  6188. transform: translate3d(-2px, 0, 0);
  6189. }
  6190.  
  6191. 20%,
  6192. 80% {
  6193. transform: translate3d(4px, 0, 0);
  6194. }
  6195.  
  6196. 30%,
  6197. 50%,
  6198. 70% {
  6199. transform: translate3d(-8px, 0, 0);
  6200. }
  6201.  
  6202. 40%,
  6203. 60% {
  6204. transform: translate3d(8px, 0, 0);
  6205. }
  6206. }
  6207.  
  6208. #column-resize-container {
  6209. >div {
  6210. align-content: center;
  6211. }
  6212. }
  6213.  
  6214. #find-favorite {
  6215. display: none;
  6216. margin-top: 7px;
  6217.  
  6218. >input {
  6219. width: 75px;
  6220. /* border-radius: 6px;
  6221. height: 35px;
  6222. border: 1px solid; */
  6223. }
  6224. }
  6225.  
  6226. #favorites-pagination-container {
  6227. padding: 0px 10px 0px 10px;
  6228.  
  6229. >button {
  6230. background: transparent;
  6231. margin: 0px 2px;
  6232. padding: 2px 6px;
  6233. border: 1px solid black;
  6234. font-size: 14px;
  6235. color: black;
  6236. font-weight: normal;
  6237.  
  6238. &:hover {
  6239. background-color: #93b393;
  6240. }
  6241.  
  6242. &.selected {
  6243. border: none !important;
  6244. font-weight: bold;
  6245. pointer-events: none;
  6246. }
  6247. }
  6248. }
  6249.  
  6250. #favorites-search-gallery-content {
  6251. padding: 0px 20px 30px 20px;
  6252. display: grid !important;
  6253. grid-template-columns: repeat(10, 1fr);
  6254. grid-gap: 0.5cqw;
  6255. }
  6256.  
  6257. #help-links-container {
  6258. >a:not(:last-child)::after {
  6259. content: " |";
  6260. }
  6261. margin-top: 17px;
  6262. }
  6263.  
  6264. #whats-new-link {
  6265. cursor: pointer;
  6266. padding: 0;
  6267. position: relative;
  6268. font-weight: bolder;
  6269. font-style: italic;
  6270. background: none;
  6271. text-decoration: none !important;
  6272.  
  6273. &.hidden:not(.persistent)>div {
  6274. display: none;
  6275. }
  6276.  
  6277. &.persistent,
  6278. &:hover {
  6279. &.light-green-gradient {
  6280. color: black;
  6281. }
  6282.  
  6283. &:not(.light-green-gradient) {
  6284. color: white;
  6285. }
  6286. }
  6287. }
  6288.  
  6289. #whats-new-container {
  6290. z-index: 10;
  6291. top: 20px;
  6292. right: 0;
  6293. transform: translateX(25%);
  6294. font-style: normal;
  6295. font-weight: normal;
  6296. white-space: nowrap;
  6297. max-width: 100vw;
  6298. padding: 5px 20px;
  6299. position: absolute;
  6300. pointer-events: none;
  6301. text-shadow: none;
  6302. border-radius: 2px;
  6303.  
  6304. &.light-green-gradient {
  6305. outline: 2px solid black;
  6306.  
  6307. }
  6308.  
  6309. &:not(.light-green-gradient) {
  6310. outline: 1.5px solid white;
  6311. }
  6312.  
  6313. ul {
  6314. padding-left: 20px;
  6315. }
  6316.  
  6317. h5,
  6318. h6 {
  6319. color: rgb(255, 0, 255);
  6320. }
  6321. }
  6322.  
  6323. .hotkey {
  6324. font-weight: bolder;
  6325. color: orange;
  6326. }
  6327.  
  6328. #left-favorites-panel-bottom-row {
  6329. display: flex;
  6330. margin-top: 10px;
  6331. flex-wrap: nowrap;
  6332.  
  6333. >div {
  6334. flex: 1;
  6335. }
  6336.  
  6337. .number {
  6338. font-size: 16px;
  6339.  
  6340. >input {
  6341. width: 5ch;
  6342. }
  6343. }
  6344. }
  6345.  
  6346. #additional-favorite-options {
  6347. >div:not(:last-child) {
  6348. margin-bottom: 10px;
  6349. }
  6350.  
  6351. select {
  6352. cursor: pointer;
  6353. min-height: 25px;
  6354. width: 150px;
  6355. }
  6356. }
  6357.  
  6358. .number-label-container {
  6359. display: inline-block;
  6360. min-width: 130px;
  6361. }
  6362.  
  6363. #show-ui-div {
  6364. &.ui-hidden {
  6365. max-width: 100vw;
  6366. text-align: center;
  6367. align-content: center;
  6368. }
  6369. }
  6370.  
  6371. #rating-container {
  6372. white-space: nowrap;
  6373. }
  6374.  
  6375. #allowed-ratings {
  6376. margin-top: 5px;
  6377. font-size: 12px;
  6378.  
  6379. >label {
  6380. outline: 1px solid;
  6381. padding: 3px;
  6382. cursor: pointer;
  6383. opacity: 0.5;
  6384. position: relative;
  6385. }
  6386.  
  6387. >label[for="explicit-rating-checkbox"] {
  6388. border-radius: 7px 0px 0px 7px;
  6389. }
  6390.  
  6391. >label[for="questionable-rating-checkbox"] {
  6392. margin-left: -3px;
  6393. }
  6394.  
  6395. >label[for="safe-rating-checkbox"] {
  6396. margin-left: -3px;
  6397. border-radius: 0px 7px 7px 0px;
  6398. }
  6399.  
  6400. >input[type="checkbox"] {
  6401. display: none;
  6402.  
  6403. &:checked+label {
  6404. background-color: #0075FF;
  6405. color: white;
  6406. opacity: 1;
  6407. }
  6408. }
  6409. }
  6410.  
  6411. .add-or-remove-button {
  6412. visibility: hidden;
  6413. cursor: pointer;
  6414. }
  6415.  
  6416. #favorites-load-status {
  6417. >label {
  6418. width: 140px;
  6419. }
  6420. }
  6421.  
  6422. #favorites-load-status-label {
  6423. /* color: #3498db; */
  6424. padding-left: 20px;
  6425. }
  6426.  
  6427. #main-favorite-options-container {
  6428. display: flex;
  6429. flex-wrap: wrap;
  6430. flex-direction: row;
  6431.  
  6432. >div {
  6433. flex-basis: 45%;
  6434. }
  6435. }
  6436.  
  6437. #sort-ascending {
  6438. position: absolute;
  6439. top: -2px;
  6440. left: 150px;
  6441. }
  6442.  
  6443. #find-favorite-input {
  6444. border: none !important;
  6445. }
  6446.  
  6447. div#header {
  6448. margin-bottom: 0 !important;
  6449. }
  6450.  
  6451. body {
  6452.  
  6453. &:fullscreen,
  6454. &::backdrop {
  6455. background-color: var(--c-bg);
  6456. }
  6457. }
  6458. </style>
  6459. <div id="favorites-search-gallery-menu-panels" style="display: flex;">
  6460. <div id="left-favorites-panel">
  6461. <h2 style="display: inline;" id="search-header">Search Favorites</h2>
  6462. <span id="favorites-load-status" style="margin-left: 5px;">
  6463. <label id="match-count-label"></label>
  6464. <label id="pagination-label" style="margin-left: 10px;"></label>
  6465. <label id="favorites-load-status-label"></label>
  6466. </span>
  6467. <div id="left-favorites-panel-top-row">
  6468. <button title="Search favorites
  6469. ctrl+click/right-click: Search all of rule34 in a new tab"
  6470. id="search-button">Search</button>
  6471. <button title="Randomize order of search results" id="shuffle-button">Shuffle</button>
  6472. <button title="Show results not matched by search" id="invert-button">Invert</button>
  6473. <button title="Empty the search box" id="clear-button">Clear</button>
  6474. <button title="Delete cached favorites and reset preferences" id="reset-button">Reset</button>
  6475. <span id="favorites-pagination-placeholder"></span>
  6476. <span id="help-links-container">
  6477. <a href="https://github.com/bruh3396/favorites-search-gallery/#controls" target="_blank">Help</a>
  6478. <a href="https://sleazyfork.org/en/scripts/504184-rule34-favorites-search-gallery/feedback"
  6479. target="_blank">Feedback</a>
  6480. <a href="https://github.com/bruh3396/favorites-search-gallery/issues" target="_blank">Report
  6481. Issue</a>
  6482. <a id="whats-new-link" href="" class="hidden light-green-gradient">What's new?
  6483.  
  6484. <div id="whats-new-container" class="light-green-gradient">
  6485. <h4>1.18:</h4>
  6486. <h5>Features:</h5>
  6487. <ul>
  6488. <li>Improved/fixed mobile UI</li>
  6489. <li>Improved mobile controls</li>
  6490. <li>Added gallery autoplay for mobile</li>
  6491. <li>Added sort by radom (auto shuffle)</li>
  6492. <li>Added dark theme option</li>
  6493. <li>Minor UI fixes</li>
  6494. <li>Minor gallery fixes</li>
  6495. </ul>
  6496. </div>
  6497. </a>
  6498. </span>
  6499. </div>
  6500. <div>
  6501. <textarea name="tags" id="favorites-search-box" placeholder="Search favorites"
  6502. spellcheck="false"></textarea>
  6503. </div>
  6504. <div id="left-favorites-panel-bottom-row">
  6505. <div id="bottom-panel-1">
  6506. <label class="checkbox" title="Show more options">
  6507. <input type="checkbox" id="options-checkbox">
  6508. <span id="more-options-label"> More Options</span>
  6509. <span class="option-hint"> (O)</span>
  6510. </label>
  6511. <div class="options-container">
  6512. <div id="main-favorite-options-container">
  6513. <div id="favorite-options">
  6514. <div>
  6515. <label class="checkbox" title="Enable gallery and other features on search pages">
  6516. <input type="checkbox" id="enable-on-search-pages">
  6517. <span> Enhance Search Pages</span>
  6518. </label>
  6519. </div>
  6520. <div style="display: none;">
  6521. <label class="checkbox" title="Toggle remove buttons">
  6522. <input type="checkbox" id="show-remove-favorite-buttons">
  6523. <span> Remove Buttons</span>
  6524. <span class="option-hint"> (R)</span>
  6525. </label>
  6526. </div>
  6527. <div style="display: none;">
  6528. <label class="checkbox" title="Toggle add favorite buttons">
  6529. <input type="checkbox" id="show-add-favorite-buttons">
  6530. <span> Add Favorite Buttons</span>
  6531. <span class="option-hint"> (R)</span>
  6532. </label>
  6533. </div>
  6534. <div>
  6535. <label class="checkbox" title="Exclude favorites with blacklisted tags from search">
  6536. <input type="checkbox" id="filter-blacklist-checkbox">
  6537. <span> Exclude Blacklist</span>
  6538. </label>
  6539. </div>
  6540. <div>
  6541. <label class="checkbox" title="Enable fancy image hovering (experimental)">
  6542. <input type="checkbox" id="fancy-image-hovering-checkbox">
  6543. <span> Fancy Hovering</span>
  6544. </label>
  6545. </div>
  6546. <div style="display: none;">
  6547. <label class="checkbox" title="Enable fancy image hovering (experimental)">
  6548. <input type="checkbox" id="statistic-hint-checkbox">
  6549. <span> Statistics</span>
  6550. <span class="option-hint"> (S)</span>
  6551. </label>
  6552. </div>
  6553. <div id="show-hints-container">
  6554. <label class="checkbox" title="Show hotkeys and shortcuts">
  6555. <input type="checkbox" id="show-hints-checkbox">
  6556. <span> Hotkey Hints</span>
  6557. <span class="option-hint"> (H)</span>
  6558. </label>
  6559. </div>
  6560. <div>
  6561. <label class="checkbox" title="Toggle dark theme">
  6562. <input type="checkbox" id="dark-theme-checkbox">
  6563. <span> Dark Theme</span>
  6564. </label>
  6565. </div>
  6566. </div>
  6567. <div id="dynamic-favorite-options">
  6568. </div>
  6569. </div>
  6570. </div>
  6571. </div>
  6572.  
  6573. <div id="bottom-panel-2">
  6574. <div id="additional-favorite-options-container" class="options-container">
  6575. <div id="additional-favorite-options">
  6576. <div id="sort-container" title="Change sorting order of search results">
  6577. <label style="margin-right: 22px;" for="sorting-method">Sort By</label>
  6578. <label style="margin-left: 22px;" for="sort-ascending">Ascending</label>
  6579. <div style="position: relative;">
  6580. <select id="sorting-method">
  6581. <option value="default">Default</option>
  6582. <option value="score">Score</option>
  6583. <option value="width">Width</option>
  6584. <option value="height">Height</option>
  6585. <option value="create">Date Uploaded</option>
  6586. <option value="change">Date Changed</option>
  6587. <option value="random">Random</option>
  6588. </select>
  6589. <input type="checkbox" id="sort-ascending">
  6590. </div>
  6591. </div>
  6592. <div id="results-columns-container">
  6593. <div id="results-per-page-container" style="display: inline-block;"
  6594. title="Set the maximum number of search results to display on each page
  6595. Lower numbers improve responsiveness">
  6596. <span class="number-label-container">
  6597. <label id="results-per-page-label" for="results-per-page-input">Results per Page</label>
  6598. </span>
  6599. <br>
  6600. <span class="number">
  6601. <hold-button class="number-arrow-down" pollingtime="50">
  6602. <span>&lt;</span>
  6603. </hold-button>
  6604. <input type="number" id="results-per-page-input" min="100" max="10000" step="50">
  6605. <hold-button class="number-arrow-up" pollingtime="50">
  6606. <span>&gt;</span>
  6607. </hold-button>
  6608. </span>
  6609. </div>
  6610. <div id="column-resize-container" title="Set the number of favorites per row"
  6611. style="display: inline-block;">
  6612. <div>
  6613. <span class="number-label-container">
  6614. <label>Columns</label>
  6615. </span>
  6616. <br>
  6617. <span class="number">
  6618. <hold-button class="number-arrow-down" pollingtime="50">
  6619. <span>&lt;</span>
  6620. </hold-button>
  6621. <input type="number" id="column-resize-input" min="2" max="20">
  6622. <hold-button class="number-arrow-up" pollingtime="50">
  6623. <span>&gt;</span>
  6624. </hold-button>
  6625. </span>
  6626. </div>
  6627. </div>
  6628. </div>
  6629. <div id="rating-container" title="Filter search results by rating">
  6630. <label>Rating</label>
  6631. <br>
  6632. <div id="allowed-ratings" class="not-highlightable">
  6633. <input type="checkbox" id="explicit-rating-checkbox" checked>
  6634. <label for="explicit-rating-checkbox">Explicit</label>
  6635. <input type="checkbox" id="questionable-rating-checkbox" checked>
  6636. <label for="questionable-rating-checkbox">Questionable</label>
  6637. <input type="checkbox" id="safe-rating-checkbox" checked>
  6638. <label for="safe-rating-checkbox" style="margin: -3px;">Safe</label>
  6639. </div>
  6640. </div>
  6641. <div id="performance-profile-container" title="Improve performance by disabling features">
  6642. <label for="performance-profile">Performance Profile</label>
  6643. <br>
  6644. <select id="performance-profile">
  6645. <option value="0">Normal</option>
  6646. <option value="1">Low (no gallery)</option>
  6647. <option value="2">Potato (only search)</option>
  6648. </select>
  6649. </div>
  6650. </div>
  6651. </div>
  6652. </div>
  6653.  
  6654. <div id="bottom-panel-3">
  6655. <div id="show-ui-div">
  6656. <label class="checkbox" title="Toggle UI">
  6657. <input type="checkbox" id="show-ui">UI
  6658. <span class="option-hint"> (U)</span>
  6659. </label>
  6660. </div>
  6661. <div class="options-container">
  6662. <span id="find-favorite">
  6663. <button title="Find favorite favorite using its ID" id="find-favorite-button"
  6664. style="white-space: nowrap;">Find</button>
  6665. <input type="number" id="find-favorite-input" placeholder="ID">
  6666. </span>
  6667. </div>
  6668. </div>
  6669.  
  6670. <div id="bottom-panel-4">
  6671.  
  6672. </div>
  6673. </div>
  6674. </div>
  6675. <div id="right-favorites-panel"></div>
  6676. </div>
  6677. <div class="loading-wheel" id="loading-wheel" style="display: none;"></div>
  6678. </div>
  6679. `;
  6680.  
  6681. static get disabled() {
  6682. return !Utils.onFavoritesPage();
  6683. }
  6684.  
  6685. static {
  6686. Utils.addStaticInitializer(() => {
  6687. if (Utils.onFavoritesPage()) {
  6688. Utils.insertFavoritesSearchGalleryHTML("afterbegin", FavoritesMenu.uiHTML);
  6689. }
  6690. });
  6691. }
  6692.  
  6693. static settings = {
  6694. mobileMenuExpandedHeight: 170,
  6695. mobileMenuBaseHeight: 56
  6696. };
  6697.  
  6698. /**
  6699. * @type {Number}
  6700. */
  6701. maxSearchHistoryLength;
  6702. /**
  6703. * @type {Object.<PropertyKey, String>}
  6704. */
  6705. preferences;
  6706. /**
  6707. * @type {Object.<PropertyKey, String>}
  6708. */
  6709. localStorageKeys;
  6710. /**
  6711. * @type {Object.<PropertyKey, HTMLButtonElement>}
  6712. */
  6713. buttons;
  6714. /**
  6715. * @type {Object.<PropertyKey, HTMLInputElement}
  6716. */
  6717. checkboxes;
  6718. /**
  6719. * @type {Object.<PropertyKey, HTMLInputElement}
  6720. */
  6721. inputs;
  6722. /**
  6723. * @type {Cooldown}
  6724. */
  6725. columnWheelResizeCaptionCooldown;
  6726. /**
  6727. * @type {String[]}
  6728. */
  6729. searchHistory;
  6730. /**
  6731. * @type {Number}
  6732. */
  6733. searchHistoryIndex;
  6734. /**
  6735. * @type {String}
  6736. */
  6737. lastSearchQuery;
  6738.  
  6739. constructor() {
  6740. if (FavoritesMenu.disabled) {
  6741. return;
  6742. }
  6743. this.initializeFields();
  6744. this.configureMobileUI();
  6745. this.extractUIElements();
  6746. this.setMainButtonInteractability(false);
  6747. this.addEventListenersToFavoritesPage();
  6748. this.loadFavoritesPagePreferences();
  6749. this.removePaginatorFromFavoritesPage();
  6750. this.configureAddOrRemoveButtonOptionVisibility();
  6751. this.configureDesktopUI();
  6752. this.addEventListenersToWhatsNewMenu();
  6753. this.addHintsOption();
  6754. }
  6755.  
  6756. initializeFields() {
  6757. this.maxSearchHistoryLength = 100;
  6758. this.searchHistory = [];
  6759. this.searchHistoryIndex = 0;
  6760. this.lastSearchQuery = "";
  6761. this.preferences = {
  6762. showAddOrRemoveButtons: Utils.userIsOnTheirOwnFavoritesPage() ? "showRemoveButtons" : "showAddFavoriteButtons",
  6763. showOptions: "showOptions",
  6764. excludeBlacklist: "excludeBlacklist",
  6765. searchHistory: "favoritesSearchHistory",
  6766. findFavorite: "findFavorite",
  6767. thumbSize: "thumbSize",
  6768. columnCount: "columnCount",
  6769. showUI: "showUI",
  6770. performanceProfile: "performanceProfile",
  6771. resultsPerPage: "resultsPerPage",
  6772. fancyImageHovering: "fancyImageHovering",
  6773. enableOnSearchPages: "enableOnSearchPages",
  6774. sortAscending: "sortAscending",
  6775. sortingMethod: "sortingMethod",
  6776. allowedRatings: "allowedRatings",
  6777. showHotkeyHints: "showHotkeyHints",
  6778. showStatisticHints: "showStatisticHints"
  6779. };
  6780. this.localStorageKeys = {
  6781. searchHistory: "favoritesSearchHistory"
  6782. };
  6783. this.columnWheelResizeCaptionCooldown = new Cooldown(500, true);
  6784. }
  6785.  
  6786. extractUIElements() {
  6787. this.buttons = {
  6788. search: document.getElementById("search-button"),
  6789. shuffle: document.getElementById("shuffle-button"),
  6790. clear: document.getElementById("clear-button"),
  6791. invert: document.getElementById("invert-button"),
  6792. reset: document.getElementById("reset-button"),
  6793. findFavorite: document.getElementById("find-favorite-button")
  6794. };
  6795. this.checkboxes = {
  6796. showOptions: document.getElementById("options-checkbox"),
  6797. showAddOrRemoveButtons: Utils.userIsOnTheirOwnFavoritesPage() ? document.getElementById("show-remove-favorite-buttons") : document.getElementById("show-add-favorite-buttons"),
  6798. filterBlacklist: document.getElementById("filter-blacklist-checkbox"),
  6799. showUI: document.getElementById("show-ui"),
  6800. fancyImageHovering: document.getElementById("fancy-image-hovering-checkbox"),
  6801. enableOnSearchPages: document.getElementById("enable-on-search-pages"),
  6802. sortAscending: document.getElementById("sort-ascending"),
  6803. explicitRating: document.getElementById("explicit-rating-checkbox"),
  6804. questionableRating: document.getElementById("questionable-rating-checkbox"),
  6805. safeRating: document.getElementById("safe-rating-checkbox"),
  6806. showHotkeyHints: document.getElementById("show-hints-checkbox"),
  6807. showStatisticHints: document.getElementById("statistic-hint-checkbox"),
  6808. darkTheme: document.getElementById("dark-theme-checkbox")
  6809. };
  6810. this.inputs = {
  6811. searchBox: document.getElementById("favorites-search-box"),
  6812. findFavorite: document.getElementById("find-favorite-input"),
  6813. columnCount: document.getElementById("column-resize-input"),
  6814. performanceProfile: document.getElementById("performance-profile"),
  6815. resultsPerPage: document.getElementById("results-per-page-input"),
  6816. sortingMethod: document.getElementById("sorting-method"),
  6817. allowedRatings: document.getElementById("allowed-ratings")
  6818. };
  6819. }
  6820.  
  6821. loadFavoritesPagePreferences() {
  6822. const userIsLoggedIn = Utils.getUserId() !== null;
  6823. const showAddOrRemoveButtonsDefault = !Utils.userIsOnTheirOwnFavoritesPage() && userIsLoggedIn;
  6824. const addOrRemoveFavoriteButtonsAreVisible = Utils.getPreference(this.preferences.showAddOrRemoveButtons, showAddOrRemoveButtonsDefault);
  6825.  
  6826. this.checkboxes.showAddOrRemoveButtons.checked = addOrRemoveFavoriteButtonsAreVisible;
  6827. setTimeout(() => {
  6828. this.toggleAddOrRemoveButtons();
  6829. }, 100);
  6830.  
  6831. const showOptions = Utils.getPreference(this.preferences.showOptions, false);
  6832.  
  6833. this.checkboxes.showOptions.checked = showOptions;
  6834. this.toggleFavoritesOptions(showOptions);
  6835.  
  6836. if (Utils.userIsOnTheirOwnFavoritesPage()) {
  6837. this.checkboxes.filterBlacklist.checked = Utils.getPreference(this.preferences.excludeBlacklist, false);
  6838. favoritesLoader.toggleTagBlacklistExclusion(this.checkboxes.filterBlacklist.checked);
  6839. } else {
  6840. this.checkboxes.filterBlacklist.checked = true;
  6841. this.checkboxes.filterBlacklist.parentElement.style.display = "none";
  6842. }
  6843. this.searchHistory = JSON.parse(localStorage.getItem(this.localStorageKeys.searchHistory)) || [];
  6844.  
  6845. if (this.searchHistory.length > 0) {
  6846. this.inputs.searchBox.value = this.searchHistory[0];
  6847. }
  6848. this.updateVisibilityOfSearchClearButton();
  6849. this.inputs.findFavorite.value = Utils.getPreference(this.preferences.findFavorite, "");
  6850. this.inputs.columnCount.value = Utils.getPreference(this.preferences.columnCount, Utils.defaults.columnCount);
  6851. this.changeColumnCount(this.inputs.columnCount.value);
  6852.  
  6853. const showUI = Utils.getPreference(this.preferences.showUI, true);
  6854.  
  6855. this.checkboxes.showUI.checked = showUI;
  6856. this.toggleUI(showUI);
  6857.  
  6858. const performanceProfile = Utils.getPerformanceProfile();
  6859.  
  6860. for (const option of this.inputs.performanceProfile.children) {
  6861. if (parseInt(option.value) === performanceProfile) {
  6862. option.selected = "selected";
  6863. }
  6864. }
  6865.  
  6866. const resultsPerPage = parseInt(Utils.getPreference(this.preferences.resultsPerPage, Utils.defaults.resultsPerPage));
  6867.  
  6868. this.changeResultsPerPage(resultsPerPage);
  6869.  
  6870. if (Utils.onMobileDevice()) {
  6871. Utils.toggleFancyImageHovering(false);
  6872. this.checkboxes.fancyImageHovering.parentElement.style.display = "none";
  6873. this.checkboxes.enableOnSearchPages.parentElement.style.display = "none";
  6874. } else {
  6875. const fancyImageHovering = Utils.getPreference(this.preferences.fancyImageHovering, false);
  6876.  
  6877. this.checkboxes.fancyImageHovering.checked = fancyImageHovering;
  6878. Utils.toggleFancyImageHovering(fancyImageHovering);
  6879. }
  6880.  
  6881. this.checkboxes.enableOnSearchPages.checked = Utils.getPreference(this.preferences.enableOnSearchPages, false);
  6882. this.checkboxes.sortAscending.checked = Utils.getPreference(this.preferences.sortAscending, false);
  6883.  
  6884. const sortingMethod = Utils.getPreference(this.preferences.sortingMethod, "default");
  6885.  
  6886. for (const option of this.inputs.sortingMethod) {
  6887. if (option.value === sortingMethod) {
  6888. option.selected = "selected";
  6889. }
  6890. }
  6891. const allowedRatings = Utils.loadAllowedRatings();
  6892.  
  6893. // eslint-disable-next-line no-bitwise
  6894. this.checkboxes.explicitRating.checked = (allowedRatings & 4) === 4;
  6895. // eslint-disable-next-line no-bitwise
  6896. this.checkboxes.questionableRating.checked = (allowedRatings & 2) === 2;
  6897. // eslint-disable-next-line no-bitwise
  6898. this.checkboxes.safeRating.checked = (allowedRatings & 1) === 1;
  6899. this.preventUserFromUncheckingAllRatings(allowedRatings);
  6900.  
  6901. const showStatisticHints = Utils.getPreference(this.preferences.showStatisticHints, false);
  6902.  
  6903. this.checkboxes.showStatisticHints.checked = showStatisticHints;
  6904. this.toggleStatisticHints(showStatisticHints);
  6905.  
  6906. this.checkboxes.darkTheme.checked = Utils.usingDarkTheme();
  6907. }
  6908.  
  6909. removePaginatorFromFavoritesPage() {
  6910. if (!Utils.onFavoritesPage()) {
  6911. return;
  6912. }
  6913. const paginator = document.getElementById("paginator");
  6914. const pi = document.getElementById("pi");
  6915.  
  6916. if (paginator !== null) {
  6917. paginator.remove();
  6918. }
  6919.  
  6920. if (pi !== null) {
  6921. pi.remove();
  6922. }
  6923. }
  6924.  
  6925. addEventListenersToFavoritesPage() {
  6926. this.buttons.search.onclick = (event) => {
  6927. const query = this.inputs.searchBox.value;
  6928.  
  6929. if (event.ctrlKey) {
  6930. const queryWithFormattedIds = query.replace(/(?:^|\s)(\d+)(?:$|\s)/g, " id:$1 ");
  6931.  
  6932. Utils.openSearchPage(queryWithFormattedIds);
  6933. } else {
  6934. Utils.hideAwesomplete(this.inputs.searchBox);
  6935. favoritesLoader.searchFavorites(query);
  6936. this.addToFavoritesSearchHistory(query);
  6937. }
  6938. };
  6939. this.buttons.search.addEventListener("contextmenu", (event) => {
  6940. const queryWithFormattedIds = this.inputs.searchBox.value.replace(/(?:^|\s)(\d+)(?:$|\s)/g, " id:$1 ");
  6941.  
  6942. Utils.openSearchPage(queryWithFormattedIds);
  6943. event.preventDefault();
  6944. });
  6945. this.inputs.searchBox.addEventListener("keydown", (event) => {
  6946. switch (event.key) {
  6947. case "Enter":
  6948. if (Utils.awesompleteIsUnselected(this.inputs.searchBox)) {
  6949. event.preventDefault();
  6950. this.buttons.search.dispatchEvent(new Event("click"));
  6951. } else {
  6952. Utils.clearAwesompleteSelection(this.inputs.searchBox);
  6953. }
  6954. break;
  6955.  
  6956. case "ArrowUp":
  6957.  
  6958. case "ArrowDown":
  6959. if (Utils.awesompleteIsVisible(this.inputs.searchBox)) {
  6960. this.updateLastSearchQuery();
  6961. } else {
  6962. event.preventDefault();
  6963. this.traverseFavoritesSearchHistory(event.key);
  6964. }
  6965. break;
  6966.  
  6967. default:
  6968. this.updateLastSearchQuery();
  6969. break;
  6970. }
  6971. });
  6972. this.inputs.searchBox.addEventListener("wheel", (event) => {
  6973. if (event.shiftKey || event.ctrlKey) {
  6974. return;
  6975. }
  6976. const direction = event.deltaY > 0 ? "ArrowDown" : "ArrowUp";
  6977.  
  6978. this.traverseFavoritesSearchHistory(direction);
  6979. event.preventDefault();
  6980. });
  6981. this.checkboxes.showOptions.onchange = () => {
  6982. this.toggleFavoritesOptions(this.checkboxes.showOptions.checked);
  6983. Utils.setPreference(this.preferences.showOptions, this.checkboxes.showOptions.checked);
  6984. };
  6985. this.checkboxes.showAddOrRemoveButtons.onchange = () => {
  6986. this.toggleAddOrRemoveButtons();
  6987. Utils.setPreference(this.preferences.showAddOrRemoveButtons, this.checkboxes.showAddOrRemoveButtons.checked);
  6988. };
  6989. this.buttons.shuffle.onclick = () => {
  6990. favoritesLoader.shuffleSearchResults();
  6991. };
  6992. this.buttons.clear.onclick = () => {
  6993. this.inputs.searchBox.value = "";
  6994.  
  6995. if (Utils.onMobileDevice()) {
  6996. this.inputs.searchBox.focus();
  6997. }
  6998. this.updateVisibilityOfSearchClearButton();
  6999. };
  7000. this.checkboxes.filterBlacklist.onchange = () => {
  7001. Utils.setPreference(this.preferences.excludeBlacklist, this.checkboxes.filterBlacklist.checked);
  7002. favoritesLoader.toggleTagBlacklistExclusion(this.checkboxes.filterBlacklist.checked);
  7003. favoritesLoader.searchFavorites();
  7004. };
  7005. this.buttons.invert.onclick = () => {
  7006. favoritesLoader.invertSearchResults();
  7007. };
  7008. this.buttons.reset.onclick = () => {
  7009. if (Utils.onMobileDevice()) {
  7010. setTimeout(() => {
  7011. Utils.deletePersistentData();
  7012. }, 10);
  7013. } else {
  7014. Utils.deletePersistentData();
  7015. }
  7016. };
  7017. this.inputs.findFavorite.addEventListener("keydown", (event) => {
  7018. if (event.key === "Enter") {
  7019. this.buttons.findFavorite.click();
  7020. }
  7021. });
  7022. this.buttons.findFavorite.onclick = () => {
  7023. favoritesLoader.findFavorite(this.inputs.findFavorite.value);
  7024. Utils.setPreference(this.preferences.findFavorite, this.inputs.findFavorite.value);
  7025. };
  7026. this.inputs.columnCount.onchange = () => {
  7027. this.changeColumnCount(parseInt(this.inputs.columnCount.value));
  7028. };
  7029. this.checkboxes.showUI.onchange = () => {
  7030. this.toggleUI(this.checkboxes.showUI.checked);
  7031. };
  7032. this.inputs.performanceProfile.onchange = () => {
  7033. Utils.setPreference(this.preferences.performanceProfile, parseInt(this.inputs.performanceProfile.value));
  7034. window.location.reload();
  7035. };
  7036. this.inputs.resultsPerPage.onchange = () => {
  7037. this.changeResultsPerPage(parseInt(this.inputs.resultsPerPage.value));
  7038. };
  7039.  
  7040. if (!Utils.onMobileDevice()) {
  7041. this.checkboxes.fancyImageHovering.onchange = () => {
  7042. Utils.toggleFancyImageHovering(this.checkboxes.fancyImageHovering.checked);
  7043. Utils.setPreference(this.preferences.fancyImageHovering, this.checkboxes.fancyImageHovering.checked);
  7044. };
  7045. }
  7046. this.checkboxes.enableOnSearchPages.onchange = () => {
  7047. Utils.setPreference(this.preferences.enableOnSearchPages, this.checkboxes.enableOnSearchPages.checked);
  7048. };
  7049. this.checkboxes.sortAscending.onchange = () => {
  7050. Utils.setPreference(this.preferences.sortAscending, this.checkboxes.sortAscending.checked);
  7051. favoritesLoader.onSortingParametersChanged();
  7052. };
  7053. this.inputs.sortingMethod.onchange = () => {
  7054. Utils.setPreference(this.preferences.sortingMethod, this.inputs.sortingMethod.value);
  7055. favoritesLoader.onSortingParametersChanged();
  7056. };
  7057. this.inputs.allowedRatings.onchange = () => {
  7058. this.changeAllowedRatings();
  7059. };
  7060. window.addEventListener("wheel", (event) => {
  7061. if (!event.shiftKey) {
  7062. return;
  7063. }
  7064. const delta = (event.wheelDelta ? event.wheelDelta : -event.deltaY);
  7065. const columnAddend = delta > 0 ? -1 : 1;
  7066.  
  7067. if (this.columnWheelResizeCaptionCooldown.ready) {
  7068. Utils.forceHideCaptions(true);
  7069. }
  7070. this.changeColumnCount(parseInt(this.inputs.columnCount.value) + columnAddend);
  7071. }, {
  7072. passive: true
  7073. });
  7074. this.columnWheelResizeCaptionCooldown.onDebounceEnd = () => {
  7075. Utils.forceHideCaptions(false);
  7076. };
  7077. this.columnWheelResizeCaptionCooldown.onCooldownEnd = () => {
  7078. if (!this.columnWheelResizeCaptionCooldown.debouncing) {
  7079. Utils.forceHideCaptions(false);
  7080. }
  7081. };
  7082. window.addEventListener("readyToSearch", () => {
  7083. this.setMainButtonInteractability(true);
  7084. }, {
  7085. once: true
  7086. });
  7087. document.addEventListener("keydown", (event) => {
  7088. if (!Utils.isHotkeyEvent(event)) {
  7089. return;
  7090. }
  7091.  
  7092. switch (event.key.toLowerCase()) {
  7093. case "r":
  7094. this.checkboxes.showAddOrRemoveButtons.click();
  7095. break;
  7096.  
  7097. case "u":
  7098. this.checkboxes.showUI.click();
  7099. break;
  7100.  
  7101. case "o":
  7102. this.checkboxes.showOptions.click();
  7103. break;
  7104.  
  7105. case "h":
  7106. this.checkboxes.showHotkeyHints.click();
  7107. break;
  7108.  
  7109. case "s":
  7110. // this.FAVORITE_CHECKBOXES.showStatisticHints.click();
  7111. break;
  7112.  
  7113. default:
  7114. break;
  7115. }
  7116. }, {
  7117. passive: true
  7118. });
  7119. window.addEventListener("load", () => {
  7120. if (!Utils.onMobileDevice()) {
  7121. this.inputs.searchBox.focus();
  7122. }
  7123. }, {
  7124. once: true
  7125. });
  7126. this.checkboxes.showStatisticHints.onchange = () => {
  7127. this.toggleStatisticHints(this.checkboxes.showStatisticHints.checked);
  7128. Utils.setPreference(this.preferences.showStatisticHints, this.checkboxes.showStatisticHints.checked);
  7129. };
  7130. window.addEventListener("searchForTag", (event) => {
  7131. this.inputs.searchBox.value = event.detail;
  7132. this.buttons.search.click();
  7133. });
  7134. this.checkboxes.darkTheme.onchange = () => {
  7135. Utils.toggleDarkTheme(this.checkboxes.darkTheme.checked);
  7136. };
  7137. }
  7138.  
  7139. configureAddOrRemoveButtonOptionVisibility() {
  7140. this.checkboxes.showAddOrRemoveButtons.parentElement.parentElement.style.display = "block";
  7141. }
  7142.  
  7143. updateLastSearchQuery() {
  7144. if (this.inputs.searchBox.value !== this.lastSearchQuery) {
  7145. this.lastSearchQuery = this.inputs.searchBox.value;
  7146. }
  7147. this.searchHistoryIndex = -1;
  7148. }
  7149.  
  7150. /**
  7151. * @param {String} newSearch
  7152. */
  7153. addToFavoritesSearchHistory(newSearch) {
  7154. newSearch = newSearch.trim();
  7155. this.searchHistory = this.searchHistory.filter(search => search !== newSearch);
  7156. this.searchHistory.unshift(newSearch);
  7157. this.searchHistory.length = Math.min(this.searchHistory.length, this.maxSearchHistoryLength);
  7158. localStorage.setItem(this.localStorageKeys.searchHistory, JSON.stringify(this.searchHistory));
  7159. }
  7160.  
  7161. /**
  7162. * @param {String} direction
  7163. */
  7164. traverseFavoritesSearchHistory(direction) {
  7165. if (this.searchHistory.length > 0) {
  7166. if (direction === "ArrowUp") {
  7167. this.searchHistoryIndex = Math.min(this.searchHistoryIndex + 1, this.searchHistory.length - 1);
  7168. } else {
  7169. this.searchHistoryIndex = Math.max(this.searchHistoryIndex - 1, -1);
  7170. }
  7171.  
  7172. if (this.searchHistoryIndex === -1) {
  7173. this.inputs.searchBox.value = this.lastSearchQuery;
  7174. } else {
  7175. this.inputs.searchBox.value = this.searchHistory[this.searchHistoryIndex];
  7176. }
  7177. }
  7178. }
  7179.  
  7180. /**
  7181. * @param {Boolean} value
  7182. */
  7183. toggleFavoritesOptions(value) {
  7184. if (Utils.onMobileDevice()) {
  7185. document.getElementById("left-favorites-panel-bottom-row").classList.toggle("hidden", !value);
  7186.  
  7187. const mobileButtonRow = document.getElementById("mobile-button-row");
  7188.  
  7189. if (mobileButtonRow !== null) {
  7190. mobileButtonRow.style.display = value ? "" : "none";
  7191. }
  7192. } else {
  7193. document.querySelectorAll(".options-container").forEach((option) => {
  7194. option.style.display = value ? "block" : "none";
  7195. });
  7196. }
  7197. }
  7198.  
  7199. toggleAddOrRemoveButtons() {
  7200. const value = this.checkboxes.showAddOrRemoveButtons.checked;
  7201.  
  7202. this.toggleAddOrRemoveButtonVisibility(value);
  7203. Utils.toggleThumbHoverOutlines(value);
  7204. Utils.forceHideCaptions(value);
  7205.  
  7206. if (!value) {
  7207. dispatchEvent(new Event("captionOverrideEnd"));
  7208. }
  7209. }
  7210.  
  7211. /**
  7212. * @param {Boolean} value
  7213. */
  7214. toggleAddOrRemoveButtonVisibility(value) {
  7215. const visibility = value ? "visible" : "hidden";
  7216.  
  7217. Utils.insertStyleHTML(`
  7218. .add-or-remove-button {
  7219. visibility: ${visibility} !important;
  7220. }
  7221. `, "add-or-remove-button-visibility");
  7222. }
  7223.  
  7224. /**
  7225. * @param {Number} count
  7226. */
  7227. changeColumnCount(count) {
  7228. count = parseInt(count);
  7229.  
  7230. if (isNaN(count)) {
  7231. this.inputs.columnCount.value = Utils.getPreference(this.preferences.columnCount, Utils.defaults.columnCount);
  7232. return;
  7233. }
  7234. const minimumColumns = Utils.onMobileDevice() ? 1 : 4;
  7235.  
  7236. count = Utils.clamp(parseInt(count), minimumColumns, 20);
  7237. Utils.insertStyleHTML(`
  7238. #favorites-search-gallery-content {
  7239. grid-template-columns: repeat(${count}, 1fr) !important;
  7240. }
  7241. `, "column-count");
  7242. this.inputs.columnCount.value = count;
  7243. Utils.setPreference(this.preferences.columnCount, count);
  7244. }
  7245.  
  7246. /**
  7247. * @param {Number} resultsPerPage
  7248. */
  7249. changeResultsPerPage(resultsPerPage) {
  7250. resultsPerPage = parseInt(resultsPerPage);
  7251.  
  7252. if (isNaN(resultsPerPage)) {
  7253. this.inputs.resultsPerPage.value = Utils.getPreference(this.preferences.resultsPerPage, Utils.defaults.resultsPerPage);
  7254. return;
  7255. }
  7256. resultsPerPage = Utils.clamp(resultsPerPage, 50, 5000);
  7257. this.inputs.resultsPerPage.value = resultsPerPage;
  7258. Utils.setPreference(this.preferences.resultsPerPage, resultsPerPage);
  7259. favoritesLoader.updateResultsPerPage(resultsPerPage);
  7260. }
  7261.  
  7262. /**
  7263. * @param {Boolean} value
  7264. */
  7265. toggleUI(value) {
  7266. const menu = document.getElementById("favorites-search-gallery-menu");
  7267. const menuPanels = document.getElementById("favorites-search-gallery-menu-panels");
  7268. const header = document.getElementById("header");
  7269. const showUIDiv = document.getElementById("show-ui-div");
  7270. const showUIContainer = document.getElementById("bottom-panel-3");
  7271.  
  7272. if (value) {
  7273. if (header !== null) {
  7274. header.style.display = "";
  7275. }
  7276. showUIContainer.insertAdjacentElement("afterbegin", showUIDiv);
  7277. menuPanels.style.display = "flex";
  7278. menu.removeAttribute("style");
  7279. } else {
  7280. menu.appendChild(showUIDiv);
  7281.  
  7282. if (header !== null) {
  7283. header.style.display = "none";
  7284. }
  7285. menuPanels.style.display = "none";
  7286. menu.style.background = getComputedStyle(document.body).background;
  7287. }
  7288. showUIDiv.classList.toggle("ui-hidden", !value);
  7289. Utils.setPreference(this.preferences.showUI, value);
  7290. }
  7291.  
  7292. configureMobileUI() {
  7293. if (!Utils.onMobileDevice()) {
  7294. return;
  7295. }
  7296. this.configureMobileStyle();
  7297. this.setupStickyMenu();
  7298. this.createMobileUIContainer();
  7299. this.createResultsPerPageSelect();
  7300. this.createColumnResizeSelect();
  7301. this.createMobileSearchBar();
  7302. this.createControlsGuide();
  7303. this.createPaginationFooter();
  7304. this.createMobileToggleSwitches();
  7305. // this.createMobileButtonRow();
  7306. this.createMobileSymbolRow();
  7307. }
  7308.  
  7309. configureMobileStyle() {
  7310. Utils.insertStyleHTML(`
  7311. #performance-profile-container,
  7312. #show-hints-container,
  7313. #whats-new-link,
  7314. #show-ui-div,
  7315. #search-header,
  7316. #left-favorites-panel-top-row {
  7317. display: none !important;
  7318. }
  7319.  
  7320. #favorites-pagination-container>button {
  7321. &:active, &:focus {
  7322. background-color: slategray;
  7323. }
  7324.  
  7325. &:hover {
  7326. background-color: transparent;
  7327. }
  7328. }
  7329.  
  7330. .thumb,
  7331. .favorite {
  7332. >div>canvas {
  7333. display: none;
  7334. }
  7335. }
  7336.  
  7337. #more-options-label {
  7338. margin-left: 6px;
  7339. }
  7340.  
  7341. .checkbox {
  7342. margin-bottom: 8px;
  7343.  
  7344. input[type="checkbox"] {
  7345. margin-right: 10px;
  7346. }
  7347. }
  7348.  
  7349. #mobile-container {
  7350. position: fixed !important;
  7351. z-index: 30;
  7352. width: 100vw;
  7353. top: 0px;
  7354. left: 0px;
  7355. }
  7356.  
  7357. #favorites-search-gallery-menu-panels {
  7358. display: block !important;
  7359. }
  7360.  
  7361. #right-favorites-panel {
  7362. margin-left: 0px !important;
  7363. }
  7364.  
  7365. #left-favorites-panel-bottom-row {
  7366. margin: 4px 0px 0px 0px !important;
  7367. }
  7368.  
  7369. #additional-favorite-options-container {
  7370. margin-right: 5px;
  7371. }
  7372.  
  7373. #favorites-search-gallery-content {
  7374. grid-gap: 1.2cqw;
  7375. }
  7376.  
  7377. #favorites-search-gallery-menu {
  7378. padding: 7px 5px 5px 5px;
  7379. top: 0;
  7380. left: 0;
  7381. width: 100vw;
  7382.  
  7383.  
  7384. &.fixed {
  7385. position: fixed;
  7386. margin-top: 0;
  7387. }
  7388. }
  7389.  
  7390. #favorites-load-status-label {
  7391. display: inline;
  7392. }
  7393.  
  7394. textarea {
  7395. border-radius: 0px;
  7396. height: 50px;
  7397. padding: 8px 0px 8px 10px !important;
  7398. }
  7399.  
  7400. body {
  7401. width: 100% !important;
  7402. }
  7403.  
  7404. #favorites-pagination-container>button {
  7405. text-align: center;
  7406. font-size: 16px;
  7407. height: 30px;
  7408. min-width: 30px;
  7409. }
  7410.  
  7411. #goto-page-input {
  7412. top: -1px;
  7413. position: relative;
  7414. height: 25px;
  7415. width: 1em !important;
  7416. text-align: center;
  7417. font-size: 16px;
  7418. }
  7419.  
  7420. #goto-page-button {
  7421. display: none;
  7422. height: 36px;
  7423. position: absolute;
  7424. margin-left: 5px;
  7425. }
  7426.  
  7427. #additional-favorite-options {
  7428. .number {
  7429. display: none;
  7430. }
  7431. }
  7432.  
  7433. #results-per-page-container {
  7434. margin-bottom: 10px;
  7435. }
  7436.  
  7437. #bottom-panel-3,
  7438. #bottom-panel-4 {
  7439. flex: none !important;
  7440. }
  7441.  
  7442. #bottom-panel-2 {
  7443. padding-top: 8px;
  7444. }
  7445.  
  7446. #rating-container {
  7447. position: relative;
  7448. left: -5px;
  7449. top: -2px;
  7450. display: none;
  7451. }
  7452.  
  7453. #favorites-pagination-container>button {
  7454. &[disabled] {
  7455. opacity: 0.25;
  7456. pointer-events: none;
  7457. }
  7458. }
  7459.  
  7460. html {
  7461. -webkit-tap-highlight-color: transparent;
  7462. -webkit-text-size-adjust: 100%;
  7463. }
  7464.  
  7465. #additional-favorite-options {
  7466. select {
  7467. width: 120px;
  7468. }
  7469. }
  7470.  
  7471. .add-or-remove-button {
  7472. filter: none;
  7473. width: 60%;
  7474. }
  7475.  
  7476. #left-favorites-panel-bottom-row {
  7477. height: ${FavoritesMenu.settings.mobileMenuExpandedHeight}px;
  7478. overflow: hidden;
  7479. transition: height 0.2s ease;
  7480. -webkit-transition: height 0.2s ease;
  7481. -moz-transition: height 0.2s ease;
  7482. -ms-transition: height 0.2s ease;
  7483. -o-transition: height 0.2s ease;
  7484. transition: height 0.2s ease;
  7485.  
  7486. &.hidden {
  7487. height: 0px;
  7488. }
  7489. }
  7490.  
  7491. #favorites-search-gallery-content.sticky {
  7492. transition: margin 0.2s ease;
  7493. }
  7494.  
  7495. #autoplay-settings-menu {
  7496. >div {
  7497. font-size: 14px !important;
  7498. }
  7499. }
  7500.  
  7501. #results-columns-container {
  7502. margin-top: -6px;
  7503. }
  7504. `, "mobile");
  7505. document.getElementById("sorting-method").parentElement.style.marginTop = "-5px";
  7506. document.getElementById("more-options-label").textContent = " Options";
  7507. document.getElementById("options-checkbox").parentElement.style.display = "none";
  7508. const experimentalLayoutEnabled = Utils.getCookie("experiment-mobile-layout", "true");
  7509.  
  7510. if (experimentalLayoutEnabled === "true") {
  7511. Utils.insertStyleHTML(`
  7512. input[type="checkbox"] {
  7513. height: 18px;
  7514. }
  7515. `, "experimental-mobile");
  7516. } else {
  7517. Utils.insertStyleHTML(`
  7518. input[type="checkbox"] {
  7519. width: 25px;
  7520. height: 25px;
  7521. }
  7522. `, "non-experimental-mobile");
  7523. }
  7524.  
  7525. if (Utils.usingIOS) {
  7526. const viewportMeta = Array.from(document.getElementsByName("viewport"))[0];
  7527.  
  7528. if (viewportMeta !== undefined) {
  7529. viewportMeta.setAttribute("content", `${viewportMeta.getAttribute("content")}, maximum-scale:1.0, user-scalable=0`);
  7530. }
  7531. }
  7532. }
  7533.  
  7534. createMobileUIContainer() {
  7535. const mobileUIContainer = document.createElement("div");
  7536. const header = document.getElementById("header");
  7537. const menu = document.getElementById("favorites-search-gallery-menu");
  7538.  
  7539. mobileUIContainer.id = "mobile-header";
  7540. Utils.favoritesSearchGalleryContainer.insertAdjacentElement("afterbegin", mobileUIContainer);
  7541.  
  7542. if (header !== null) {
  7543. mobileUIContainer.appendChild(header);
  7544. }
  7545. mobileUIContainer.appendChild(menu);
  7546. }
  7547.  
  7548. setupStickyMenu() {
  7549. const header = document.getElementById("header");
  7550. const headerHeight = header === null ? 0 : header.getBoundingClientRect().height;
  7551.  
  7552. window.addEventListener("scroll", async() => {
  7553. if (window.scrollY > headerHeight && document.getElementById("sticky-header-fsg-style") === null) {
  7554. Utils.insertStyleHTML(
  7555. `
  7556. #favorites-search-gallery-menu {
  7557. position: fixed;
  7558. margin-top: 0;
  7559. }
  7560. `,
  7561. "sticky-header"
  7562. );
  7563. this.updateOptionContentMargin();
  7564. await Utils.sleep(1);
  7565. document.getElementById("favorites-search-gallery-content").classList.add("sticky");
  7566.  
  7567. } else if (window.scrollY <= headerHeight && document.getElementById("sticky-header-fsg-style") !== null) {
  7568. document.getElementById("sticky-header-fsg-style").remove();
  7569. document.getElementById("favorites-search-gallery-content").classList.remove("sticky");
  7570. this.removeOptionContentMargin();
  7571. }
  7572. }, {
  7573. passive: true
  7574. });
  7575. }
  7576.  
  7577. createResultsPerPageSelect() {
  7578. const resultsPerPageSelectHTML = `
  7579. <select id="results-per-page-select">
  7580. <option value="50">50</option>
  7581. <option value="100">100</option>
  7582. <option value="200">200</option>
  7583. <option value="500">500</option>
  7584. <option value="1000">1000</option>
  7585. <option value="2000">2000</option>
  7586. <option value="5000">5000</option>
  7587. </select>
  7588. `;
  7589.  
  7590. document.getElementById("results-per-page-container").querySelector(".number")
  7591. .insertAdjacentHTML("afterend", resultsPerPageSelectHTML);
  7592. const resultsPerPageSelect = document.getElementById("results-per-page-select");
  7593.  
  7594. resultsPerPageSelect.value = Utils.getPreference(this.preferences.resultsPerPage, Utils.defaults.resultsPerPage);
  7595. resultsPerPageSelect.onchange = () => {
  7596. this.changeResultsPerPage(parseInt(resultsPerPageSelect.value));
  7597. };
  7598. }
  7599.  
  7600. createColumnResizeSelect() {
  7601. const columnResizeSelect = document.createElement("select");
  7602. const columnResizeNumberInput = document.getElementById("column-resize-container").querySelector(".number");
  7603.  
  7604. for (let i = 1; i <= 10; i += 1) {
  7605. const option = document.createElement("option");
  7606.  
  7607. option.value = i;
  7608. option.textContent = i;
  7609. columnResizeSelect.appendChild(option);
  7610. }
  7611. columnResizeSelect.value = Utils.getPreference(this.preferences.columnCount, Utils.defaults.columnCount);
  7612. columnResizeSelect.onchange = () => {
  7613. this.changeColumnCount(parseInt(columnResizeSelect.value));
  7614. };
  7615. columnResizeNumberInput.insertAdjacentElement("afterend", columnResizeSelect);
  7616. }
  7617.  
  7618. createMobileSearchBar() {
  7619. document.getElementById("clear-button").remove();
  7620. document.getElementById("search-button").remove();
  7621. document.getElementById("options-checkbox").remove();
  7622. document.getElementById("reset-button").remove();
  7623.  
  7624. Utils.insertStyleHTML(`
  7625. #mobile-toolbar-row {
  7626. display: flex;
  7627. align-items: center;
  7628. background: none;
  7629.  
  7630. svg {
  7631. fill: black;
  7632. -webkit-transition: none;
  7633. transition: none;
  7634. transform: scale(0.85);
  7635. }
  7636.  
  7637. input[type="checkbox"]:checked + label {
  7638. svg {
  7639. fill: #0075FF;
  7640. }
  7641. color: #0075FF;
  7642. }
  7643.  
  7644. .dark-green-gradient {
  7645. svg {
  7646. fill: white;
  7647. }
  7648. }
  7649. }
  7650. .search-bar-container {
  7651. align-content: center;
  7652. width: 100%;
  7653. height: 40px;
  7654. border-radius: 50px;
  7655. padding-left: 10px;
  7656. padding-right: 10px;
  7657. flex: 1;
  7658. background: white;
  7659.  
  7660. &.dark-green-gradient {
  7661. background: #303030;
  7662. }
  7663. }
  7664.  
  7665. .search-bar-items {
  7666. display: flex;
  7667. align-items: center;
  7668. height: 100%;
  7669. width: 100%;
  7670.  
  7671. > div {
  7672. flex: 0;
  7673. min-width: 40px;
  7674. width: 100%;
  7675. height: 100%;
  7676. display: block;
  7677. align-content: center;
  7678. }
  7679. }
  7680.  
  7681. .search-icon-container {
  7682. flex: 0;
  7683. min-width: 40px;
  7684. }
  7685.  
  7686. .search-bar-input-container {
  7687. flex: 1 !important;
  7688. display: flex;
  7689. width: 100%;
  7690. height: 100%;
  7691. }
  7692.  
  7693. .search-bar-input {
  7694. flex: 1;
  7695. border: none;
  7696. box-sizing: content-box;
  7697. height: 100%;
  7698. padding: 0;
  7699. margin: 0;
  7700. outline: none !important;
  7701. border: none !important;
  7702. font-size: 14px !important;
  7703. width: 100%;
  7704.  
  7705. &:focus, &:focus-visible {
  7706. background: none !important;
  7707. border: none !important;
  7708. outline: none !important;
  7709. }
  7710. }
  7711.  
  7712. .search-clear-container {
  7713. visibility: hidden;
  7714.  
  7715. svg {
  7716. transition: none !important;
  7717. transform: scale(0.6) !important;
  7718. }
  7719. }
  7720.  
  7721. .circle-icon-container {
  7722. padding: 0;
  7723. margin: 0;
  7724. align-content: center;
  7725. border-radius: 50%;
  7726.  
  7727. &:active {
  7728. background-color: #0075FF;
  7729. }
  7730. }
  7731.  
  7732. #options-checkbox {
  7733. display: none;
  7734. }
  7735.  
  7736. .mobile-toolbar-checkbox-label {
  7737. width: 100%;
  7738. height: 100%;
  7739. display: block;
  7740. }
  7741.  
  7742. #reset-button {
  7743. transition: none !important;
  7744. height: 100%;
  7745.  
  7746. >svg {
  7747. transition: none !important;
  7748. transform: scale(0.65);
  7749. }
  7750.  
  7751. &:active {
  7752. svg {
  7753. fill: #0075FF;
  7754. }
  7755. }
  7756. }
  7757.  
  7758. #help-button {
  7759. height: 100%;
  7760.  
  7761. >svg {
  7762. transform: scale(0.75);
  7763. }
  7764. }
  7765.  
  7766. .
  7767. `, "mobile-toolbar");
  7768.  
  7769. const searchBar = document.getElementById("favorites-search-box");
  7770. const mobileSearchBarHTML = `
  7771. <div id="mobile-toolbar-row" class="light-green-gradient">
  7772. <div class="search-bar-container light-green-gradient">
  7773. <div class="search-bar-items">
  7774. <div>
  7775. <div class="circle-icon-container">
  7776. <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"/>
  7777. </svg>
  7778. </div>
  7779. </div>
  7780. <div class="search-bar-input-container">
  7781. <input type="text" id="favorites-search-box" class="search-bar-input" needs-autocomplete placeholder="Search favorites">
  7782. </div>
  7783. <div class="toolbar-button search-clear-container">
  7784. <div class="circle-icon-container">
  7785. <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"/>
  7786. </svg>
  7787. </div>
  7788. </div>
  7789. <div>
  7790. <input type="checkbox" id="options-checkbox">
  7791. <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>
  7792. </div>
  7793. <div>
  7794. <div id="reset-button">
  7795. <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>
  7796. </div>
  7797. </div>
  7798. <div style="display: none;">
  7799. <div id="">
  7800. <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>
  7801. </div>
  7802. </div>
  7803. </div>
  7804. </div>
  7805. </div>
  7806. `;
  7807.  
  7808. searchBar.insertAdjacentHTML("afterend", mobileSearchBarHTML);
  7809. searchBar.remove();
  7810. document.getElementById("favorites-search-box").addEventListener("input", () => {
  7811. this.updateVisibilityOfSearchClearButton();
  7812. });
  7813. document.getElementById("options-checkbox").addEventListener("change", (event) => {
  7814. const menuIsSticky = document.getElementById("favorites-search-gallery-content").classList.contains("sticky");
  7815. const margin = event.target.checked ? FavoritesMenu.settings.mobileMenuBaseHeight + FavoritesMenu.settings.mobileMenuExpandedHeight : FavoritesMenu.settings.mobileMenuBaseHeight;
  7816.  
  7817. if (menuIsSticky) {
  7818. Utils.sleep(1);
  7819. this.updateOptionContentMargin(margin);
  7820. }
  7821. });
  7822. }
  7823.  
  7824. createPaginationFooter() {
  7825. Utils.insertStyleHTML(`
  7826. #mobile-footer {
  7827. position: fixed;
  7828. width: 100%;
  7829. bottom: 0;
  7830. left: 0;
  7831. padding: 4px 0px;
  7832. > div {
  7833. text-align: center;
  7834. }
  7835.  
  7836. &.light-green-gradient {
  7837. background: linear-gradient(to top, #aae5a4, #89e180);
  7838. }
  7839. &.dark-green-gradient {
  7840. background: linear-gradient(to top, #5e715e, #293129);
  7841.  
  7842. }
  7843. }
  7844.  
  7845. #mobile-footer-top {
  7846. margin-bottom: 4px;
  7847. }
  7848.  
  7849. #favorites-search-gallery-content {
  7850. margin-bottom: 20px;
  7851. }
  7852.  
  7853. #favorites-load-status {
  7854. font-size: 12px !important;
  7855. >span {
  7856. margin-right: 10px;
  7857. }
  7858.  
  7859. >span:nth-child(odd) {
  7860. font-weight: bold;
  7861. }
  7862. }
  7863.  
  7864. #favorites-load-status-label {
  7865. padding-left: 0 !important;
  7866. }
  7867.  
  7868. #pagination-number:active {
  7869. opacity: 0.5;
  7870. filter: none !important;
  7871. }
  7872. `, "mobile-footer");
  7873. const footerHTML = `
  7874. <div id="mobile-footer" class="light-green-gradient">
  7875. <div id="mobile-footer-header"></div>
  7876. <div id="mobile-footer-top"></div>
  7877. <div id="mobile-footer-bottom"></div>
  7878. </div>
  7879. `;
  7880. const loadStatus = document.getElementById("favorites-load-status");
  7881.  
  7882. for (const label of Array.from(loadStatus.querySelectorAll("label"))) {
  7883. const span = document.createElement("span");
  7884.  
  7885. span.id = label.id;
  7886. span.className = label.className;
  7887. span.innerHTML = label.innerHTML;
  7888. label.remove();
  7889. loadStatus.appendChild(span);
  7890. }
  7891. Utils.insertFavoritesSearchGalleryHTML("beforeend", footerHTML);
  7892. const footerHeader = document.getElementById("mobile-footer-header");
  7893. const footerTop = document.getElementById("mobile-footer-top");
  7894. const footerBottom = document.getElementById("mobile-footer-bottom");
  7895.  
  7896. footerHeader.appendChild(document.getElementById("help-links-container"));
  7897. footerTop.appendChild(document.getElementById("favorites-load-status"));
  7898. footerBottom.appendChild(document.getElementById("favorites-pagination-placeholder"));
  7899. document.getElementById("whats-new-link").remove();
  7900. }
  7901.  
  7902. createControlsGuide() {
  7903. Utils.insertStyleHTML(`
  7904. #controls-guide {
  7905. display: none;
  7906. z-index: 99999;
  7907. --tap-control: blue;
  7908. --swipe-down: red;
  7909. --swipe-up: green;
  7910. top: 0;
  7911. left: 0;
  7912. background: lightblue;
  7913. width: 100%;
  7914. height: 100%;
  7915. padding: 0;
  7916. margin: 0;
  7917. flex-direction: column;
  7918. position: fixed;
  7919.  
  7920. &.active {
  7921. display: flex;
  7922. }
  7923. }
  7924.  
  7925. #controls-guide-image-container {
  7926. background: black;
  7927. width: 100%;
  7928. height: 100%;
  7929. }
  7930.  
  7931. #controls-guide-sample-image {
  7932. background: lightblue;
  7933. position: relative;
  7934. top: 50%;
  7935. left: 0;
  7936. width: 100%;
  7937. transform: translateY(-50%);
  7938. }
  7939.  
  7940. #controls-guide-top {
  7941. position: relative;
  7942. flex: 3;
  7943. }
  7944.  
  7945. #controls-guide-bottom {
  7946. flex: 1;
  7947. min-height: 25%;
  7948. padding: 10px;
  7949. font-size: 20px;
  7950. align-content: center;
  7951. }
  7952.  
  7953. #controls-guide-tap-container {
  7954. width: 100%;
  7955. height: 100%;
  7956. position: absolute;
  7957. }
  7958. .controls-guide-tap {
  7959. color: white;
  7960. font-size: 50px;
  7961. position: absolute;
  7962. top: 50%;
  7963. height: 65%;
  7964. width: 15%;
  7965. background: var(--tap-control);
  7966. z-index: 9999;
  7967. transform: translateY(-50%);
  7968. writing-mode: vertical-lr;
  7969. text-align: center;
  7970. opacity: 0.8;
  7971. }
  7972.  
  7973. #controls-guide-tap-right {
  7974. right: 0;
  7975. }
  7976. #controls-guide-tap-left {
  7977. left: 0;
  7978. }
  7979. #controls-guide-swipe-container {
  7980. position: absolute;
  7981. top: 0;
  7982. left: 0;
  7983. width: 100%;
  7984. height: 100%;
  7985.  
  7986. svg {
  7987. position: absolute;
  7988. left: 50%;
  7989. transform: translateX(-50%);
  7990. width: 25%;
  7991. }
  7992. }
  7993.  
  7994. #controls-guide-swipe-down {
  7995. top: 0;
  7996. color: var(--swipe-down);
  7997. fill: var(--swipe-down);
  7998. }
  7999.  
  8000. #controls-guide-swipe-up {
  8001. bottom: 0;
  8002. color: var(--swipe-up);
  8003. fill: var(--swipe-up);
  8004. }
  8005. `, "controls-guide");
  8006. Utils.insertFavoritesSearchGalleryHTML("beforeend", `
  8007. <div id="controls-guide">
  8008. <div id="controls-guide-top">
  8009. <div id="controls-guide-tap-container">
  8010. <div id="controls-guide-tap-left" class="controls-guide-tap">
  8011. Previous
  8012. </div>
  8013. <div id="controls-guide-tap-right" class="controls-guide-tap">
  8014. Next
  8015. </div>
  8016. </div>
  8017. <div id="controls-guide-image-container">
  8018. <img id="controls-guide-sample-image" src="https://rule34.xxx/images/header2.png">
  8019. </div>
  8020. <div id="controls-guide-swipe-container">
  8021. <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>
  8022. <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>
  8023. </div>
  8024. </div>
  8025. <div id="controls-guide-bottom">
  8026. <ul style="text-align: center; list-style: none;">
  8027. <li style="color: var(--tap-control);">Tap edges to traverse gallery</li>
  8028. <li style="color: var(--swipe-down);">Swipe down to exit gallery</li>
  8029. <li style="color: var(--swipe-up);">Swipe up to open autoplay menu</li>
  8030. </ul>
  8031. </div>
  8032. </div>
  8033. `);
  8034. const controlGuide = document.getElementById("controls-guide");
  8035. const anchor = document.createElement("a");
  8036.  
  8037. anchor.textContent = "Controls";
  8038. anchor.href = "#";
  8039. anchor.onmousedown = (event) => {
  8040. event.preventDefault();
  8041. event.stopPropagation();
  8042. controlGuide.classList.toggle("active", true);
  8043. };
  8044. controlGuide.ontouchstart = (event) => {
  8045. event.preventDefault();
  8046. event.stopPropagation();
  8047. controlGuide.classList.toggle("active", false);
  8048. };
  8049.  
  8050. document.getElementById("help-links-container").insertAdjacentElement("afterbegin", anchor);
  8051. controlGuide.onmousedown = () => {
  8052. controlGuide.classList.toggle("active", false);
  8053. };
  8054. }
  8055.  
  8056. createMobileToggleSwitches() {
  8057. window.addEventListener("postProcess", () => {
  8058. setTimeout(() => {
  8059. this.createMobileToggleSwitchesHelper();
  8060. }, 10);
  8061. }, {
  8062. once: true
  8063. });
  8064. }
  8065.  
  8066. createMobileToggleSwitchesHelper() {
  8067. Utils.insertStyleHTML(`
  8068. .toggle-switch {
  8069. position: relative;
  8070. display: inline-block;
  8071. width: 60px;
  8072. height: 34px;
  8073. transform: scale(.75);
  8074. align-content: center;
  8075. }
  8076.  
  8077. .toggle-switch input {
  8078. opacity: 0;
  8079. width: 0;
  8080. height: 0;
  8081. }
  8082.  
  8083. .slider {
  8084. position: absolute;
  8085. cursor: pointer;
  8086. top: 0;
  8087. left: 0;
  8088. right: 0;
  8089. bottom: 0;
  8090. background-color: #ccc;
  8091. -webkit-transition: .4s;
  8092. transition: .4s;
  8093. }
  8094.  
  8095. .slider:before {
  8096. position: absolute;
  8097. content: "";
  8098. height: 26px;
  8099. width: 26px;
  8100. left: 4px;
  8101. bottom: 4px;
  8102. background-color: white;
  8103. -webkit-transition: .4s;
  8104. transition: .4s;
  8105. }
  8106.  
  8107. input:checked + .slider {
  8108. background-color: #0075FF;
  8109. }
  8110.  
  8111. input:focus + .slider {
  8112. box-shadow: 0 0 1px #0075FF;
  8113. }
  8114.  
  8115. input:checked + .slider:before {
  8116. -webkit-transform: translateX(26px);
  8117. -ms-transform: translateX(26px);
  8118. transform: translateX(26px);
  8119. }
  8120.  
  8121. .slider.round {
  8122. border-radius: 34px;
  8123. }
  8124.  
  8125. .slider.round:before {
  8126. border-radius: 50%;
  8127. }
  8128.  
  8129. .toggle-switch-label {
  8130. margin-left: 60px;
  8131. margin-top: 20px;
  8132. font-size: 16px;
  8133. }
  8134.  
  8135. #sort-ascending {
  8136. width: 0 !important;
  8137. height: 0 !important;
  8138. position: static !important;
  8139. }
  8140.  
  8141. `, "mobile-toggle-switch");
  8142. const checkboxes = Array.from(document.querySelectorAll(".checkbox"))
  8143. .filter(checkbox => checkbox.getBoundingClientRect().width > 0);
  8144.  
  8145. for (const hint of Array.from(document.querySelectorAll(".option-hint"))) {
  8146. hint.remove();
  8147. }
  8148.  
  8149. for (const checkbox of checkboxes) {
  8150. const label = checkbox.querySelector("span");
  8151. const input = checkbox.querySelector("input");
  8152. const slider = document.createElement("span");
  8153.  
  8154. if (input === null) {
  8155. continue;
  8156. }
  8157. slider.className = "slider round";
  8158. checkbox.className = "toggle-switch";
  8159. input.insertAdjacentElement("afterend", slider);
  8160.  
  8161. if (label !== null) {
  8162. label.className = "toggle-switch-label";
  8163. }
  8164. }
  8165. const sortAscendingCheckbox = document.getElementById("sort-ascending");
  8166.  
  8167. if (sortAscendingCheckbox !== null) {
  8168. const container = document.createElement("span");
  8169. const toggleSwitch = document.createElement("label");
  8170. const slider = document.createElement("span");
  8171.  
  8172. toggleSwitch.className = "toggle-switch";
  8173. toggleSwitch.style.transform = "scale(0.6)";
  8174. toggleSwitch.style.marginLeft = "-12px";
  8175. slider.className = "slider round";
  8176. sortAscendingCheckbox.insertAdjacentElement("beforebegin", container);
  8177. container.appendChild(toggleSwitch);
  8178. toggleSwitch.appendChild(sortAscendingCheckbox);
  8179. toggleSwitch.appendChild(slider);
  8180. sortAscendingCheckbox.insertAdjacentElement("afterend", slider);
  8181. }
  8182. }
  8183.  
  8184. createMobileButtonRow() {
  8185. const buttonHeight = 30;
  8186.  
  8187. Utils.insertStyleHTML(`
  8188. #mobile-button-row {
  8189. padding: 0;
  8190. position: absolute;
  8191. width: 98%;
  8192. display: flex;
  8193. gap: 10px;
  8194. padding: 0px 20px;
  8195.  
  8196. >button, >div {
  8197. font-size: 20px;
  8198. flex: 1;
  8199. height: ${buttonHeight}px;
  8200. border-radius: 30px;
  8201. }
  8202. }
  8203.  
  8204. #left-favorites-panel-bottom-row>div:not(:first-child) {
  8205. margin-top:${buttonHeight}px
  8206. }
  8207. `, "mobile-button");
  8208.  
  8209. const html = `
  8210. <div id="mobile-button-row">
  8211. <button>Reset</button>
  8212. <button>Help</button>
  8213. <button>Shuffle</button>
  8214. </div>
  8215. `;
  8216.  
  8217. document.getElementById("left-favorites-panel-bottom-row").insertAdjacentHTML("afterbegin", html);
  8218. }
  8219.  
  8220. createMobileSymbolRow() {
  8221. Utils.insertStyleHTML(`
  8222. #mobile-symbol-container {
  8223. display: flex;
  8224. gap: 10px;
  8225. text-align: center;
  8226. height: 0;
  8227. overflow: hidden;
  8228. width: 100%;
  8229. transition: height .2s ease;
  8230.  
  8231. >button {
  8232. font-size: 20px;
  8233. padding: 0;
  8234. margin: 0;
  8235. font-weight: bold;
  8236. text-align: center;
  8237. flex: 1;
  8238. height: 100% !important;
  8239. }
  8240.  
  8241. &.active {
  8242. height: 30px;
  8243. }
  8244. }
  8245. `);
  8246. document.getElementById("left-favorites-panel")
  8247. .insertAdjacentHTML("afterbegin", `
  8248. <div id="mobile-symbol-container">
  8249. <button>-</button>
  8250. <button>*</button>
  8251. <button>_</button>
  8252. <button>(</button>
  8253. <button>)</button>
  8254. <button>~</button>
  8255. </div>
  8256. `);
  8257. const mobileSymbolContainer = document.getElementById("mobile-symbol-container");
  8258. /**
  8259. * @type {HTMLInputElement}
  8260. */
  8261.  
  8262. const searchBar = document.getElementById("favorites-search-box");
  8263.  
  8264. for (const button of Array.from(document.getElementById("mobile-symbol-container").querySelectorAll("button"))) {
  8265. button.addEventListener("blur", async(event) => {
  8266. await Utils.sleep(0);
  8267.  
  8268. if (document.activeElement.id !== "favorites-search-box" && !mobileSymbolContainer.contains(document.activeElement)) {
  8269. mobileSymbolContainer.classList.toggle("active", false);
  8270. }
  8271. });
  8272.  
  8273. button.addEventListener("click", () => {
  8274. const value = searchBar.value;
  8275. const selectionStart = searchBar.selectionStart;
  8276.  
  8277. searchBar.value = value.slice(0, selectionStart) + button.textContent + value.slice(selectionStart);
  8278. this.updateVisibilityOfSearchClearButton();
  8279. searchBar.selectionStart = selectionStart + 1;
  8280. searchBar.selectionEnd = selectionStart + 1;
  8281. searchBar.focus();
  8282. }, {
  8283. passive: true
  8284. });
  8285. }
  8286.  
  8287. window.addEventListener("postProcess", () => {
  8288.  
  8289. searchBar.addEventListener("focus", () => {
  8290. document.getElementById("mobile-symbol-container").classList.toggle("active", true);
  8291. }, {
  8292. passive: true
  8293. });
  8294.  
  8295. searchBar.addEventListener("blur", async(event) => {
  8296. await Utils.sleep(10);
  8297.  
  8298. if (document.activeElement.id !== "favorites-search-box" && !mobileSymbolContainer.contains(document.activeElement)) {
  8299. mobileSymbolContainer.classList.toggle("active", false);
  8300. }
  8301. });
  8302. }, {
  8303. once: true
  8304. });
  8305. }
  8306.  
  8307. clickedOnSearchItem(event) {
  8308.  
  8309. }
  8310.  
  8311. updateVisibilityOfSearchClearButton() {
  8312. if (!Utils.onMobileDevice()) {
  8313. return;
  8314. }
  8315. const clearButtonContainer = document.querySelector(".search-clear-container");
  8316.  
  8317. if (clearButtonContainer === null) {
  8318. return;
  8319. }
  8320.  
  8321. const clearButtonIsHidden = getComputedStyle(clearButtonContainer).visibility === "hidden";
  8322. const searchBarIsEmpty = this.inputs.searchBox.value === "";
  8323. const styleId = "search-clear-button-visibility";
  8324.  
  8325. if (searchBarIsEmpty && !clearButtonIsHidden) {
  8326. Utils.insertStyleHTML(".search-clear-container {visibility: hidden}", styleId);
  8327. } else if (!searchBarIsEmpty && clearButtonIsHidden) {
  8328. Utils.insertStyleHTML(".search-clear-container {visibility: visible}", styleId);
  8329. }
  8330. }
  8331.  
  8332. /**
  8333. * @param {Number} margin
  8334. */
  8335. updateOptionContentMargin(margin) {
  8336. margin = margin === undefined ? document.getElementById("favorites-search-gallery-menu").getBoundingClientRect().height + 11 : margin;
  8337. Utils.insertStyleHTML(`
  8338. #favorites-search-gallery-content {
  8339. margin-top: ${margin}px;
  8340. }`, "options-content-margin");
  8341. }
  8342.  
  8343. removeOptionContentMargin() {
  8344. const optionsContentMargin = document.getElementById("options-content-margin-fsg-style");
  8345.  
  8346. if (optionsContentMargin !== null) {
  8347. optionsContentMargin.remove();
  8348. }
  8349. }
  8350.  
  8351. configureDesktopUI() {
  8352. if (Utils.onMobileDevice()) {
  8353. return;
  8354. }
  8355. Utils.insertStyleHTML(`
  8356. .checkbox {
  8357. &:hover {
  8358. color: #000;
  8359. background: #93b393;
  8360. text-shadow: none;
  8361. cursor: pointer;
  8362. }
  8363.  
  8364. input[type="checkbox"] {
  8365. width: 20px;
  8366. height: 20px;
  8367. }
  8368. }
  8369.  
  8370. #sort-ascending {
  8371. width: 20px;
  8372. height: 20px;
  8373. }
  8374. `, "desktop");
  8375. }
  8376.  
  8377. addEventListenersToWhatsNewMenu() {
  8378. if (Utils.onMobileDevice()) {
  8379. return;
  8380. }
  8381. const whatsNew = document.getElementById("whats-new-link");
  8382.  
  8383. if (whatsNew === null) {
  8384. return;
  8385. }
  8386. whatsNew.onclick = () => {
  8387. if (whatsNew.classList.contains("persistent")) {
  8388. whatsNew.classList.remove("persistent");
  8389. whatsNew.classList.add("hidden");
  8390. } else {
  8391. whatsNew.classList.add("persistent");
  8392. }
  8393. return false;
  8394. };
  8395.  
  8396. whatsNew.onblur = () => {
  8397. whatsNew.classList.remove("persistent");
  8398. whatsNew.classList.add("hidden");
  8399. };
  8400.  
  8401. whatsNew.onmouseenter = () => {
  8402. whatsNew.classList.remove("hidden");
  8403. };
  8404.  
  8405. whatsNew.onmouseleave = () => {
  8406. whatsNew.classList.add("hidden");
  8407. };
  8408. }
  8409.  
  8410. changeAllowedRatings() {
  8411. let allowedRatings = 0;
  8412.  
  8413. if (this.checkboxes.explicitRating.checked) {
  8414. allowedRatings += 4;
  8415. }
  8416.  
  8417. if (this.checkboxes.questionableRating.checked) {
  8418. allowedRatings += 2;
  8419. }
  8420.  
  8421. if (this.checkboxes.safeRating.checked) {
  8422. allowedRatings += 1;
  8423. }
  8424.  
  8425. Utils.setPreference(this.preferences.allowedRatings, allowedRatings);
  8426. favoritesLoader.onAllowedRatingsChanged(allowedRatings);
  8427. this.preventUserFromUncheckingAllRatings(allowedRatings);
  8428. }
  8429.  
  8430. /**
  8431. * @param {Number} allowedRatings
  8432. */
  8433. preventUserFromUncheckingAllRatings(allowedRatings) {
  8434. if (allowedRatings === 4) {
  8435. this.checkboxes.explicitRating.nextElementSibling.style.pointerEvents = "none";
  8436. } else if (allowedRatings === 2) {
  8437. this.checkboxes.questionableRating.nextElementSibling.style.pointerEvents = "none";
  8438. } else if (allowedRatings === 1) {
  8439. this.checkboxes.safeRating.nextElementSibling.style.pointerEvents = "none";
  8440. } else {
  8441. this.checkboxes.explicitRating.nextElementSibling.removeAttribute("style");
  8442. this.checkboxes.questionableRating.nextElementSibling.removeAttribute("style");
  8443. this.checkboxes.safeRating.nextElementSibling.removeAttribute("style");
  8444. }
  8445. }
  8446.  
  8447. setMainButtonInteractability(value) {
  8448. const container = document.getElementById("left-favorites-panel-top-row");
  8449.  
  8450. if (container === null) {
  8451. return;
  8452. }
  8453. const mainButtons = Array.from(container.children).filter(child => child.tagName.toLowerCase() === "button" && child.textContent !== "Reset");
  8454.  
  8455. for (const button of mainButtons) {
  8456. button.disabled = !value;
  8457. }
  8458. }
  8459.  
  8460. /**
  8461. * @param {Boolean} value
  8462. */
  8463. toggleOptionHints(value) {
  8464. const html = value ? "" : ".option-hint {display:none;}";
  8465.  
  8466. Utils.insertStyleHTML(html, "option-hint-visibility");
  8467. }
  8468.  
  8469. async addHintsOption() {
  8470. this.toggleOptionHints(false);
  8471.  
  8472. await Utils.sleep(50);
  8473.  
  8474. if (Utils.onMobileDevice()) {
  8475. return;
  8476. }
  8477. const optionHintsEnabled = Utils.getPreference(this.preferences.showHotkeyHints, false);
  8478.  
  8479. this.checkboxes.showHotkeyHints.checked = optionHintsEnabled;
  8480. this.checkboxes.showHotkeyHints.onchange = () => {
  8481. this.toggleOptionHints(this.checkboxes.showHotkeyHints.checked);
  8482. Utils.setPreference(this.preferences.showHotkeyHints, this.checkboxes.showHotkeyHints.checked);
  8483. };
  8484. this.toggleOptionHints(optionHintsEnabled);
  8485. }
  8486.  
  8487. /**
  8488. * @param {Boolean} value
  8489. */
  8490. toggleStatisticHints(value) {
  8491. const html = value ? "" : ".statistic-hint {display:none;}";
  8492.  
  8493. Utils.insertStyleHTML(html, "statistic-hint-visibility");
  8494. }
  8495. }
  8496.  
  8497. class AutoplayListenerList {
  8498. /**
  8499. * @type {Function}
  8500. */
  8501. onEnable;
  8502. /**
  8503. * @type {Function}
  8504. */
  8505. onDisable;
  8506. /**
  8507. * @type {Function}
  8508. */
  8509. onPause;
  8510. /**
  8511. * @type {Function}
  8512. */
  8513. onResume;
  8514. /**
  8515. * @type {Function}
  8516. */
  8517. onComplete;
  8518. /**
  8519. * @type {Function}
  8520. */
  8521. onVideoEndedBeforeMinimumViewTime;
  8522.  
  8523. /**
  8524. * @param {Function} onEnable
  8525. * @param {Function} onDisable
  8526. * @param {Function} onPause
  8527. * @param {Function} onResume
  8528. * @param {Function} onComplete
  8529. * @param {Function} onVideoEndedEarly
  8530. */
  8531. constructor(onEnable, onDisable, onPause, onResume, onComplete, onVideoEndedEarly) {
  8532. this.onEnable = onEnable;
  8533. this.onDisable = onDisable;
  8534. this.onPause = onPause;
  8535. this.onResume = onResume;
  8536. this.onComplete = onComplete;
  8537. this.onVideoEndedBeforeMinimumViewTime = onVideoEndedEarly;
  8538. }
  8539. }
  8540.  
  8541. class Autoplay {
  8542. static autoplayHTML = `
  8543. <div id="autoplay-container">
  8544. <style>
  8545. #autoplay-container {
  8546. visibility: hidden;
  8547. }
  8548.  
  8549. #autoplay-menu {
  8550. position: fixed;
  8551. left: 50%;
  8552. transform: translate(-50%);
  8553. bottom: 5%;
  8554. padding: 0;
  8555. margin: 0;
  8556. background: rgba(40, 40, 40, 1);
  8557. border-radius: 4px;
  8558. white-space: nowrap;
  8559. z-index: 10000;
  8560. opacity: 0;
  8561. transition: opacity .25s ease-in-out;
  8562.  
  8563. &.visible {
  8564. opacity: 1;
  8565. }
  8566.  
  8567. &.persistent {
  8568. opacity: 1 !important;
  8569. visibility: visible !important;
  8570. }
  8571.  
  8572. >div>img {
  8573. color: red;
  8574. position: relative;
  8575. height: 75px;
  8576. cursor: pointer;
  8577. background-color: rgba(128, 128, 128, 0);
  8578. margin: 5px;
  8579. background-size: 10%;
  8580. z-index: 3;
  8581. border-radius: 4px;
  8582.  
  8583.  
  8584. &:hover {
  8585. background-color: rgba(200, 200, 200, .5);
  8586. }
  8587. }
  8588. }
  8589.  
  8590. .autoplay-progress-bar {
  8591. position: absolute;
  8592. top: 0;
  8593. left: 0;
  8594. width: 0%;
  8595. height: 100%;
  8596. background-color: steelblue;
  8597. z-index: 1;
  8598. }
  8599.  
  8600. #autoplay-video-progress-bar {
  8601. background-color: royalblue;
  8602. }
  8603.  
  8604. #autoplay-settings-menu {
  8605. visibility: hidden;
  8606. position: absolute;
  8607. top: 0;
  8608. left: 50%;
  8609. transform: translate(-50%, -105%);
  8610. border-radius: 4px;
  8611. font-size: 10px !important;
  8612. background: rgba(40, 40, 40, 1);
  8613.  
  8614. &.visible {
  8615. visibility: visible;
  8616. }
  8617.  
  8618. >div {
  8619. font-size: 30px;
  8620. display: flex;
  8621. justify-content: space-between;
  8622. align-items: center;
  8623. padding: 5px 10px;
  8624. color: white;
  8625.  
  8626.  
  8627. >label {
  8628. padding-right: 20px;
  8629. }
  8630.  
  8631. >.number {
  8632. background: none;
  8633. outline: 2px solid white;
  8634.  
  8635. >hold-button,
  8636. >button {
  8637. &::after {
  8638. width: 200%;
  8639. height: 130%;
  8640. }
  8641. }
  8642.  
  8643. >input[type="number"] {
  8644. color: white;
  8645. width: 7ch;
  8646. }
  8647. }
  8648. }
  8649.  
  8650. select {
  8651. /* height: 25px; */
  8652. font-size: larger;
  8653. width: 10ch;
  8654. }
  8655. }
  8656.  
  8657. #autoplay-settings-button.settings-menu-opened {
  8658. filter: drop-shadow(6px 6px 3px #0075FF);
  8659. }
  8660.  
  8661.  
  8662. #autoplay-change-direction-mask {
  8663. filter: drop-shadow(2px 2px 3px #0075FF);
  8664. }
  8665.  
  8666. #autoplay-play-button:active {
  8667. filter: drop-shadow(2px 2px 10px #0075FF);
  8668. }
  8669.  
  8670. #autoplay-change-direction-mask-container {
  8671. pointer-events: none;
  8672. opacity: 0.75;
  8673. height: 75px;
  8674. width: 75px;
  8675. margin: 5px;
  8676. border-radius: 4px;
  8677. right: 0;
  8678. bottom: 0;
  8679. z-index: 4;
  8680. position: absolute;
  8681. clip-path: polygon(0% 0%, 0% 100%, 100% 100%);
  8682.  
  8683. &.upper-right {
  8684. clip-path: polygon(0% 0%, 100% 0%, 100% 100%);
  8685. }
  8686. }
  8687.  
  8688. .autoplay-settings-menu-label {
  8689. pointer-events: none;
  8690. }
  8691. </style>
  8692. <div id="autoplay-menu" class="not-highlightable">
  8693. <div id="autoplay-buttons">
  8694. <img id="autoplay-settings-button" title="Autoplay settings">
  8695. <img id="autoplay-play-button" title="Pause autoplay">
  8696. <img id="autoplay-change-direction-button" title="Change autoplay direction">
  8697. <div id="autoplay-change-direction-mask-container">
  8698. <img id="autoplay-change-direction-mask" title="Change autoplay direction">
  8699. </div>
  8700. </div>
  8701. <div id="autoplay-image-progress-bar" class="autoplay-progress-bar"></div>
  8702. <div id="autoplay-video-progress-bar" class="autoplay-progress-bar"></div>
  8703. <div id="autoplay-settings-menu">
  8704. <div>
  8705. <label for="autoplay-image-duration-input">Image/GIF Duration</label>
  8706. <span class="number">
  8707. <hold-button class="number-arrow-down" pollingtime="100"><span>&lt;</span></hold-button>
  8708. <input type="number" id="autoplay-image-duration-input" min="1" max="60" step="1">
  8709. <hold-button class="number-arrow-up" pollingtime="100"><span>&gt;</span></hold-button>
  8710. </span>
  8711. </div>
  8712. <div>
  8713. <label for="autoplay-minimum-video-duration-input">Minimum Video Duration</label>
  8714. <span class="number">
  8715. <hold-button class="number-arrow-down" pollingtime="100"><span>&lt;</span></hold-button>
  8716. <input type="number" id="autoplay-minimum-animated-duration-input" min="1" max="60" step="1">
  8717. <hold-button class="number-arrow-up" pollingtime="100"><span>&gt;</span></hold-button>
  8718. </span>
  8719. </div>
  8720. </div>
  8721. </div>
  8722. </div>
  8723. `;
  8724. static preferences = {
  8725. active: "autoplayActive",
  8726. paused: "autoplayPaused",
  8727. imageDuration: "autoplayImageDuration",
  8728. minimumVideoDuration: "autoplayMinimumVideoDuration",
  8729. direction: "autoplayForward"
  8730. };
  8731. static menuIconImageURLs = {
  8732. play: Utils.createObjectURLFromSvg(Utils.icons.play),
  8733. pause: Utils.createObjectURLFromSvg(Utils.icons.pause),
  8734. changeDirection: Utils.createObjectURLFromSvg(Utils.icons.changeDirection),
  8735. changeDirectionAlt: Utils.createObjectURLFromSvg(Utils.icons.changeDirectionAlt),
  8736. tune: Utils.createObjectURLFromSvg(Utils.icons.tune)
  8737. };
  8738. static settings = {
  8739. imageViewDuration: Utils.getPreference(Autoplay.preferences.imageDuration, 3000),
  8740. minimumVideoDuration: Utils.getPreference(Autoplay.preferences.minimumVideoDuration, 5000),
  8741. menuVisibilityDuration: Utils.onMobileDevice() ? 1500 : 500,
  8742. moveForward: Utils.getPreference(Autoplay.preferences.direction, true),
  8743.  
  8744. get imageViewDurationInSeconds() {
  8745. return Utils.millisecondsToSeconds(this.imageViewDuration);
  8746. },
  8747.  
  8748. get minimumVideoDurationInSeconds() {
  8749. return Utils.millisecondsToSeconds(this.minimumVideoDuration);
  8750. }
  8751. };
  8752.  
  8753. /**
  8754. * @type {Boolean}
  8755. */
  8756. static get disabled() {
  8757. return false;
  8758. // return Utils.onMobileDevice();
  8759. }
  8760.  
  8761. /**
  8762. * @type {{
  8763. * container: HTMLDivElement,
  8764. * menu: HTMLDivElement,
  8765. * settingsButton: HTMLImageElement,
  8766. * settingsMenu: {
  8767. * container: HTMLDivElement
  8768. * imageDurationInput: HTMLInputElement,
  8769. * minimumVideoDurationInput: HTMLInputElement,
  8770. * }
  8771. * playButton: HTMLImageElement,
  8772. * changeDirectionButton: HTMLImageElement,
  8773. * changeDirectionMask: {
  8774. * container: HTMLDivElement,
  8775. * image: HTMLImageElement
  8776. * },
  8777. * imageProgressBar: HTMLDivElement
  8778. * videoProgressBar: HTMLDivElement
  8779. * }}
  8780. */
  8781. ui;
  8782. /**
  8783. * @type {AutoplayListenerList}
  8784. */
  8785. events;
  8786. /**
  8787. * @type {AbortController}
  8788. */
  8789. eventListenersAbortController;
  8790. /**
  8791. * @type {HTMLElement}
  8792. */
  8793. currentThumb;
  8794. /**
  8795. * @type {Cooldown}
  8796. */
  8797. imageViewTimer;
  8798. /**
  8799. * @type {Cooldown}
  8800. */
  8801. menuVisibilityTimer;
  8802. /**
  8803. * @type {Cooldown}
  8804. */
  8805. videoViewTimer;
  8806. /**
  8807. * @type {Boolean}
  8808. */
  8809. active;
  8810. /**
  8811. * @type {Boolean}
  8812. */
  8813. paused;
  8814. /**
  8815. * @type {Boolean}
  8816. */
  8817. menuIsPersistent;
  8818. /**
  8819. * @type {Boolean}
  8820. */
  8821. menuIsVisible;
  8822.  
  8823. /**
  8824. * @param {AutoplayListenerList} events
  8825. */
  8826. constructor(events) {
  8827. if (Autoplay.disabled) {
  8828. return;
  8829. }
  8830. this.initializeEvents(events);
  8831. this.initializeFields();
  8832. this.initializeTimers();
  8833. this.insertHTML();
  8834. this.configureMobileUi();
  8835. this.extractUiElements();
  8836. this.setMenuIconImageSources();
  8837. this.loadAutoplaySettingsIntoUI();
  8838. this.addEventListeners();
  8839. }
  8840.  
  8841. /**
  8842. * @param {AutoplayListenerList} events
  8843. */
  8844. initializeEvents(events) {
  8845. this.events = events;
  8846.  
  8847. const onComplete = events.onComplete;
  8848.  
  8849. this.events.onComplete = () => {
  8850. if (this.active && !this.paused) {
  8851. onComplete();
  8852. }
  8853. };
  8854. }
  8855.  
  8856. initializeFields() {
  8857. this.ui = {
  8858. settingsMenu: {},
  8859. changeDirectionMask: {}
  8860. };
  8861. this.eventListenersAbortController = new AbortController();
  8862. this.currentThumb = null;
  8863. this.active = Utils.getPreference(Autoplay.preferences.active, Utils.onMobileDevice());
  8864. this.paused = Utils.getPreference(Autoplay.preferences.paused, false);
  8865. this.menuIsPersistent = false;
  8866. this.menuIsVisible = false;
  8867. }
  8868.  
  8869. initializeTimers() {
  8870. this.imageViewTimer = new Cooldown(Autoplay.settings.imageViewDuration);
  8871. this.menuVisibilityTimer = new Cooldown(Autoplay.settings.menuVisibilityDuration);
  8872. this.videoViewTimer = new Cooldown(Autoplay.settings.minimumVideoDuration);
  8873.  
  8874. this.imageViewTimer.onCooldownEnd = () => { };
  8875. this.menuVisibilityTimer.onCooldownEnd = () => {
  8876. this.hideMenu();
  8877. setTimeout(() => {
  8878. if (!this.menuIsPersistent && !this.menuIsVisible) {
  8879. this.toggleSettingMenu(false);
  8880. }
  8881. }, 100);
  8882. };
  8883. }
  8884.  
  8885. insertHTML() {
  8886. this.insertMenuHTML();
  8887. this.insertOptionHTML();
  8888. this.insertImageProgressHTML();
  8889. this.insertVideoProgressHTML();
  8890. }
  8891.  
  8892. insertMenuHTML() {
  8893. Utils.insertFavoritesSearchGalleryHTML("afterbegin", Autoplay.autoplayHTML);
  8894. }
  8895.  
  8896. insertOptionHTML() {
  8897. Utils.createFavoritesOption(
  8898. "autoplay",
  8899. "Autoplay",
  8900. "Enable autoplay in gallery",
  8901. this.active,
  8902. (event) => {
  8903. this.toggle(event.target.checked);
  8904. },
  8905. true
  8906. );
  8907. }
  8908.  
  8909. insertImageProgressHTML() {
  8910. Utils.insertStyleHTML(`
  8911. #autoplay-image-progress-bar.animated {
  8912. transition: width ${Autoplay.settings.imageViewDurationInSeconds}s linear;
  8913. width: 100%;
  8914. }
  8915. `, "autoplay-image-progress-bar-animation");
  8916. }
  8917.  
  8918. insertVideoProgressHTML() {
  8919. Utils.insertStyleHTML(`
  8920. #autoplay-video-progress-bar.animated {
  8921. transition: width ${Autoplay.settings.minimumVideoDurationInSeconds}s linear;
  8922. width: 100%;
  8923. }
  8924. `, "autoplay-video-progress-bar-animation");
  8925. }
  8926.  
  8927. extractUiElements() {
  8928. this.ui.container = document.getElementById("autoplay-container");
  8929. this.ui.menu = document.getElementById("autoplay-menu");
  8930. this.ui.settingsButton = document.getElementById("autoplay-settings-button");
  8931. this.ui.settingsMenu.container = document.getElementById("autoplay-settings-menu");
  8932. this.ui.settingsMenu.imageDurationInput = document.getElementById("autoplay-image-duration-input");
  8933. this.ui.settingsMenu.minimumVideoDurationInput = document.getElementById("autoplay-minimum-animated-duration-input");
  8934. this.ui.playButton = document.getElementById("autoplay-play-button");
  8935. this.ui.changeDirectionButton = document.getElementById("autoplay-change-direction-button");
  8936. this.ui.changeDirectionMask.container = document.getElementById("autoplay-change-direction-mask-container");
  8937. this.ui.changeDirectionMask.image = document.getElementById("autoplay-change-direction-mask");
  8938. this.ui.imageProgressBar = document.getElementById("autoplay-image-progress-bar");
  8939. this.ui.videoProgressBar = document.getElementById("autoplay-video-progress-bar");
  8940. }
  8941.  
  8942. configureMobileUi() {
  8943. this.createViewDurationSelects();
  8944. }
  8945.  
  8946. createViewDurationSelects() {
  8947. const imageViewDurationSelect = this.createDurationSelect(1, 60);
  8948. const videoViewDurationSelect = this.createDurationSelect(0, 60);
  8949. const imageViewDurationInput = document.getElementById("autoplay-image-duration-input").parentElement;
  8950. const videoViewDurationInput = document.getElementById("autoplay-minimum-animated-duration-input").parentElement;
  8951.  
  8952. imageViewDurationSelect.value = Autoplay.settings.imageViewDurationInSeconds;
  8953. videoViewDurationSelect.value = Autoplay.settings.minimumVideoDurationInSeconds;
  8954. imageViewDurationInput.insertAdjacentElement("afterend", imageViewDurationSelect);
  8955. videoViewDurationInput.insertAdjacentElement("afterend", videoViewDurationSelect);
  8956. imageViewDurationInput.remove();
  8957. videoViewDurationInput.remove();
  8958. imageViewDurationSelect.id = "autoplay-image-duration-input";
  8959. videoViewDurationSelect.id = "autoplay-minimum-animated-duration-input";
  8960. }
  8961.  
  8962. /**
  8963. * @param {Number} minimum
  8964. * @param {Number} maximum
  8965. * @returns {HTMLSelectElement}
  8966. */
  8967. createDurationSelect(minimum, maximum) {
  8968. const select = document.createElement("select");
  8969.  
  8970. for (let i = minimum; i <= maximum; i += 1) {
  8971. const option = document.createElement("option");
  8972.  
  8973. switch (true) {
  8974. case i <= 5:
  8975. break;
  8976.  
  8977. case i <= 20:
  8978. i += 4;
  8979. break;
  8980.  
  8981. case i <= 30:
  8982. i += 9;
  8983. break;
  8984.  
  8985. default:
  8986. i += 29;
  8987. break;
  8988. }
  8989. option.value = i;
  8990. option.innerText = i;
  8991. select.append(option);
  8992. }
  8993. select.ontouchstart = () => {
  8994. select.dispatchEvent(new Event("mousedown"));
  8995. };
  8996. return select;
  8997. }
  8998.  
  8999. setMenuIconImageSources() {
  9000. this.ui.playButton.src = this.paused ? Autoplay.menuIconImageURLs.play : Autoplay.menuIconImageURLs.pause;
  9001. this.ui.settingsButton.src = Autoplay.menuIconImageURLs.tune;
  9002. this.ui.changeDirectionButton.src = Autoplay.menuIconImageURLs.changeDirection;
  9003. this.ui.changeDirectionMask.image.src = Autoplay.menuIconImageURLs.changeDirectionAlt;
  9004. this.ui.changeDirectionMask.container.classList.toggle("upper-right", Autoplay.settings.moveForward);
  9005. }
  9006.  
  9007. loadAutoplaySettingsIntoUI() {
  9008. this.ui.settingsMenu.imageDurationInput.value = Autoplay.settings.imageViewDurationInSeconds;
  9009. this.ui.settingsMenu.minimumVideoDurationInput.value = Autoplay.settings.minimumVideoDurationInSeconds;
  9010. }
  9011.  
  9012. addEventListeners() {
  9013. this.addMenuEventListeners();
  9014. this.addSettingsMenuEventListeners();
  9015. }
  9016.  
  9017. addMenuEventListeners() {
  9018. this.addDesktopMenuEventListeners();
  9019. this.addMobileMenuEventListeners();
  9020. }
  9021.  
  9022. addDesktopMenuEventListeners() {
  9023. if (Utils.onMobileDevice()) {
  9024. return;
  9025. }
  9026. this.ui.settingsButton.onclick = () => {
  9027. this.toggleSettingMenu();
  9028. };
  9029. this.ui.playButton.onclick = () => {
  9030. this.pause();
  9031. };
  9032. this.ui.changeDirectionButton.onclick = () => {
  9033. this.toggleDirection();
  9034. };
  9035. this.ui.menu.onmouseenter = () => {
  9036. this.toggleMenuPersistence(true);
  9037. };
  9038. this.ui.menu.onmouseleave = () => {
  9039. this.toggleMenuPersistence(false);
  9040. };
  9041. }
  9042.  
  9043. addMobileMenuEventListeners() {
  9044. if (!Utils.onMobileDevice()) {
  9045. return;
  9046. }
  9047. this.ui.settingsButton.ontouchstart = () => {
  9048. this.toggleSettingMenu();
  9049. const settingsMenuIsVisible = this.ui.settingsMenu.container.classList.contains("visible");
  9050.  
  9051. this.toggleMenuPersistence(settingsMenuIsVisible);
  9052. this.menuVisibilityTimer.restart();
  9053. };
  9054. this.ui.playButton.ontouchstart = () => {
  9055. this.pause();
  9056. this.menuVisibilityTimer.restart();
  9057. };
  9058. this.ui.changeDirectionButton.ontouchstart = () => {
  9059. this.toggleDirection();
  9060. this.menuVisibilityTimer.restart();
  9061. };
  9062. }
  9063.  
  9064. addSettingsMenuEventListeners() {
  9065. this.ui.settingsMenu.imageDurationInput.onchange = () => {
  9066. this.setImageViewDuration();
  9067.  
  9068. if (this.currentThumb !== null && Utils.isImage(this.currentThumb)) {
  9069. this.startViewTimer(this.currentThumb);
  9070. }
  9071. };
  9072. this.ui.settingsMenu.minimumVideoDurationInput.onchange = () => {
  9073. this.setMinimumVideoViewDuration();
  9074.  
  9075. if (this.currentThumb !== null && !Utils.isImage(this.currentThumb)) {
  9076. this.startViewTimer(this.currentThumb);
  9077. }
  9078. };
  9079. }
  9080.  
  9081. /**
  9082. * @param {Boolean} forward
  9083. */
  9084. toggleDirection(forward) {
  9085. const directionHasNotChanged = forward === Autoplay.settings.moveForward;
  9086.  
  9087. if (directionHasNotChanged) {
  9088. return;
  9089. }
  9090. Autoplay.settings.moveForward = !Autoplay.settings.moveForward;
  9091. this.ui.changeDirectionMask.container.classList.toggle("upper-right", Autoplay.settings.moveForward);
  9092. Utils.setPreference(Autoplay.preferences.direction, Autoplay.settings.moveForward);
  9093. }
  9094.  
  9095. /**
  9096. * @param {Boolean} value
  9097. */
  9098. toggleMenuPersistence(value) {
  9099. this.menuIsPersistent = value;
  9100. this.ui.menu.classList.toggle("persistent", value);
  9101. }
  9102.  
  9103. /**
  9104. * @param {Boolean} value
  9105. */
  9106. toggleMenuVisibility(value) {
  9107. this.menuIsVisible = value;
  9108. this.ui.menu.classList.toggle("visible", value);
  9109. }
  9110.  
  9111. /**
  9112. * @param {Boolean} value
  9113. */
  9114. toggleSettingMenu(value) {
  9115. if (value === undefined) {
  9116. this.ui.settingsMenu.container.classList.toggle("visible");
  9117. this.ui.settingsButton.classList.toggle("settings-menu-opened");
  9118. } else {
  9119. this.ui.settingsMenu.container.classList.toggle("visible", value);
  9120. this.ui.settingsButton.classList.toggle("settings-menu-opened", value);
  9121. }
  9122. }
  9123.  
  9124. /**
  9125. * @param {Boolean} value
  9126. */
  9127. toggle(value) {
  9128. Utils.setPreference(Autoplay.preferences.active, value);
  9129. this.active = value;
  9130.  
  9131. if (value) {
  9132. this.events.onEnable();
  9133. } else {
  9134. this.events.onDisable();
  9135. }
  9136. }
  9137.  
  9138. setImageViewDuration() {
  9139. let durationInSeconds = parseFloat(this.ui.settingsMenu.imageDurationInput.value);
  9140.  
  9141. if (isNaN(durationInSeconds)) {
  9142. durationInSeconds = Autoplay.settings.imageViewDurationInSeconds;
  9143. }
  9144. const duration = Math.round(Utils.clamp(durationInSeconds * 1000, 1000, 60000));
  9145.  
  9146. Utils.setPreference(Autoplay.preferences.imageDuration, duration);
  9147. Autoplay.settings.imageViewDuration = duration;
  9148. this.imageViewTimer.waitTime = duration;
  9149. this.ui.settingsMenu.imageDurationInput.value = Autoplay.settings.imageViewDurationInSeconds;
  9150. this.insertImageProgressHTML();
  9151. }
  9152.  
  9153. setMinimumVideoViewDuration() {
  9154. let durationInSeconds = parseFloat(this.ui.settingsMenu.minimumVideoDurationInput.value);
  9155.  
  9156. if (isNaN(durationInSeconds)) {
  9157. durationInSeconds = Autoplay.settings.minimumVideoDurationInSeconds;
  9158. }
  9159. const duration = Math.round(Utils.clamp(durationInSeconds * 1000, 0, 60000));
  9160.  
  9161. Utils.setPreference(Autoplay.preferences.minimumVideoDuration, duration);
  9162. Autoplay.settings.minimumVideoDuration = duration;
  9163. this.videoViewTimer.waitTime = duration;
  9164. this.ui.settingsMenu.minimumVideoDurationInput.value = Autoplay.settings.minimumVideoDurationInSeconds;
  9165. this.insertVideoProgressHTML();
  9166. }
  9167.  
  9168. /**
  9169. * @param {HTMLElement} thumb
  9170. */
  9171. startViewTimer(thumb) {
  9172. if (thumb === null) {
  9173. return;
  9174. }
  9175. this.currentThumb = thumb;
  9176.  
  9177. if (!this.active || Autoplay.disabled || this.paused) {
  9178. return;
  9179. }
  9180.  
  9181. if (Utils.isVideo(thumb)) {
  9182. this.startVideoViewTimer();
  9183. } else {
  9184. this.startImageViewTimer();
  9185. }
  9186. }
  9187.  
  9188. startImageViewTimer() {
  9189. this.stopVideoProgressBar();
  9190. this.stopVideoViewTimer();
  9191. this.startImageProgressBar();
  9192. this.imageViewTimer.restart();
  9193. }
  9194.  
  9195. stopImageViewTimer() {
  9196. this.imageViewTimer.stop();
  9197. this.stopImageProgressBar();
  9198. }
  9199.  
  9200. startVideoViewTimer() {
  9201. this.stopImageViewTimer();
  9202. this.stopImageProgressBar();
  9203. this.startVideoProgressBar();
  9204. this.videoViewTimer.restart();
  9205. }
  9206.  
  9207. stopVideoViewTimer() {
  9208. this.videoViewTimer.stop();
  9209. this.stopVideoProgressBar();
  9210. }
  9211.  
  9212. /**
  9213. * @param {HTMLElement} thumb
  9214. */
  9215. start(thumb) {
  9216. if (!this.active || Autoplay.disabled) {
  9217. return;
  9218. }
  9219. this.addAutoplayEventListeners();
  9220. this.ui.container.style.visibility = "visible";
  9221. this.showMenu();
  9222. this.startViewTimer(thumb);
  9223. }
  9224.  
  9225. stop() {
  9226. if (Autoplay.disabled) {
  9227. return;
  9228. }
  9229. this.ui.container.style.visibility = "hidden";
  9230. this.removeAutoplayEventListeners();
  9231. this.stopImageViewTimer();
  9232. this.stopVideoViewTimer();
  9233. this.forceHideMenu();
  9234. }
  9235.  
  9236. pause() {
  9237. this.paused = !this.paused;
  9238. Utils.setPreference(Autoplay.preferences.paused, this.paused);
  9239.  
  9240. if (this.paused) {
  9241. this.ui.playButton.src = Autoplay.menuIconImageURLs.play;
  9242. this.ui.playButton.title = "Resume Autoplay";
  9243. this.stopImageViewTimer();
  9244. this.stopVideoViewTimer();
  9245. this.events.onPause();
  9246. } else {
  9247. this.ui.playButton.src = Autoplay.menuIconImageURLs.pause;
  9248. this.ui.playButton.title = "Pause Autoplay";
  9249. this.startViewTimer(this.currentThumb);
  9250. this.events.onResume();
  9251. }
  9252. }
  9253.  
  9254. onVideoEnded() {
  9255. if (this.videoViewTimer.timeout === null) {
  9256. this.events.onComplete();
  9257. } else {
  9258. this.events.onVideoEndedBeforeMinimumViewTime();
  9259. }
  9260. }
  9261.  
  9262. addAutoplayEventListeners() {
  9263. this.imageViewTimer.onCooldownEnd = () => {
  9264. this.events.onComplete();
  9265. };
  9266. document.addEventListener("mousemove", () => {
  9267. this.showMenu();
  9268. }, {
  9269. signal: this.eventListenersAbortController.signal
  9270. });
  9271. document.addEventListener("keydown", (event) => {
  9272. if (!Utils.isHotkeyEvent(event)) {
  9273. return;
  9274. }
  9275.  
  9276. switch (event.key.toLowerCase()) {
  9277. case "p":
  9278. this.showMenu();
  9279. this.pause();
  9280. break;
  9281.  
  9282. case " ":
  9283. if (this.currentThumb !== null && !Utils.isVideo(this.currentThumb)) {
  9284. this.showMenu();
  9285. this.pause();
  9286. }
  9287. break;
  9288.  
  9289. default:
  9290. break;
  9291. }
  9292. }, {
  9293. signal: this.eventListenersAbortController.signal
  9294. });
  9295. }
  9296.  
  9297. removeAutoplayEventListeners() {
  9298. this.imageViewTimer.onCooldownEnd = () => { };
  9299. this.eventListenersAbortController.abort();
  9300. this.eventListenersAbortController = new AbortController();
  9301. }
  9302.  
  9303. showMenu() {
  9304. this.toggleMenuVisibility(true);
  9305. this.menuVisibilityTimer.restart();
  9306. }
  9307.  
  9308. hideMenu() {
  9309. this.toggleMenuVisibility(false);
  9310. }
  9311.  
  9312. forceHideMenu() {
  9313. this.toggleMenuPersistence(false);
  9314. this.toggleMenuVisibility(false);
  9315. this.toggleSettingMenu(false);
  9316. }
  9317.  
  9318. startImageProgressBar() {
  9319. this.stopImageProgressBar();
  9320. setTimeout(() => {
  9321. this.ui.imageProgressBar.classList.add("animated");
  9322. }, 10);
  9323. }
  9324.  
  9325. stopImageProgressBar() {
  9326. this.ui.imageProgressBar.classList.remove("animated");
  9327. }
  9328.  
  9329. startVideoProgressBar() {
  9330. this.stopVideoProgressBar();
  9331. setTimeout(() => {
  9332. this.ui.videoProgressBar.classList.add("animated");
  9333. }, 10);
  9334. }
  9335.  
  9336. stopVideoProgressBar() {
  9337. this.ui.videoProgressBar.classList.remove("animated");
  9338. }
  9339. }
  9340.  
  9341. class VideoClip {
  9342. /**
  9343. * @type {Number}
  9344. */
  9345. start;
  9346. /**
  9347. * @type {Number}
  9348. */
  9349. end;
  9350.  
  9351. /**
  9352. * @param {{start: Number, end: Number}} videoClip
  9353. */
  9354. constructor(videoClip) {
  9355. this.start = videoClip.start;
  9356. this.end = videoClip.end;
  9357. }
  9358. }
  9359.  
  9360. class Gallery {
  9361. static galleryHTML = `
  9362. <style>
  9363. body {
  9364. width: 99.5vw;
  9365. overflow-x: hidden;
  9366. }
  9367.  
  9368. .focused {
  9369. transition: none;
  9370. float: left;
  9371. overflow: hidden;
  9372. z-index: 9997;
  9373. pointer-events: none;
  9374. position: fixed;
  9375. height: 100vh;
  9376. margin: 0;
  9377. top: 50%;
  9378. left: 50%;
  9379. transform: translate(-50%, -50%);
  9380. }
  9381.  
  9382. #gallery-container {
  9383.  
  9384. >canvas,
  9385. img {
  9386. float: left;
  9387. overflow: hidden;
  9388. pointer-events: none;
  9389. position: fixed;
  9390. height: 100vh;
  9391. margin: 0;
  9392. top: 50%;
  9393. left: 50%;
  9394. transform: translate(-50%, -50%);
  9395. }
  9396. }
  9397.  
  9398. #original-video-container {
  9399. cursor: default;
  9400.  
  9401. video {
  9402. top: 0;
  9403. left: 0;
  9404. display: none;
  9405. position: fixed;
  9406. z-index: 9998;
  9407. }
  9408. }
  9409.  
  9410. #low-resolution-canvas {
  9411. z-index: 9996;
  9412. }
  9413.  
  9414. #main-canvas {
  9415. z-index: 9997;
  9416. }
  9417.  
  9418. a.hide {
  9419. cursor: default;
  9420. }
  9421.  
  9422. option {
  9423. font-size: 15px;
  9424. }
  9425.  
  9426. #resolution-dropdown {
  9427. text-align: center;
  9428. width: 160px;
  9429. height: 25px;
  9430. cursor: pointer;
  9431. }
  9432.  
  9433. #original-content-background {
  9434. position: fixed;
  9435. top: 0;
  9436. left: 0;
  9437. width: 100%;
  9438. height: 100%;
  9439. background: black;
  9440. z-index: 999;
  9441. display: none;
  9442. pointer-events: none;
  9443. cursor: default;
  9444. -webkit-user-drag: none;
  9445. -khtml-user-drag: none;
  9446. -moz-user-drag: none;
  9447. -o-user-drag: none;
  9448. }
  9449.  
  9450. #original-content-background-link-mask {
  9451. position: fixed;
  9452. top: 0;
  9453. left: 0;
  9454. width: 100%;
  9455. height: 100%;
  9456. background: red;
  9457. z-index: 10001;
  9458. pointer-events: none;
  9459. cursor: default;
  9460. display: none;
  9461. opacity: 0;
  9462. -webkit-user-drag: none;
  9463. -khtml-user-drag: none;
  9464. -moz-user-drag: none;
  9465. -o-user-drag: none;
  9466.  
  9467. &.active {
  9468. /* opacity: 0.2; */
  9469. pointer-events: all;
  9470. }
  9471. }
  9472.  
  9473. #original-gif-container {
  9474. z-index: 9995;
  9475. }
  9476. </style>
  9477. `;
  9478. static galleryDebugHTML = `
  9479. .thumb,
  9480. .favorite {
  9481. &.debug-selected {
  9482. outline: 3px solid #0075FF !important;
  9483. }
  9484.  
  9485. &.loaded {
  9486.  
  9487. div, a {
  9488. outline: 2px solid transparent;
  9489. animation: outlineGlow 1s forwards;
  9490. }
  9491.  
  9492. .image {
  9493. opacity: 1;
  9494. }
  9495. }
  9496.  
  9497. >a
  9498. >canvas {
  9499. position: absolute;
  9500. top: 0;
  9501. left: 0;
  9502. pointer-events: none;
  9503. z-index: 1;
  9504. visibility: hidden;
  9505. }
  9506.  
  9507. .image {
  9508. opacity: 0.4;
  9509. transition: transform 0.1s ease-in-out, opacity 0.5s ease;
  9510. }
  9511.  
  9512. }
  9513.  
  9514. .image.loaded {
  9515. animation: outlineGlow 1s forwards;
  9516. opacity: 1;
  9517. }
  9518.  
  9519. @keyframes outlineGlow {
  9520. 0% {
  9521. outline-color: transparent;
  9522. }
  9523.  
  9524. 100% {
  9525. outline-color: turquoise;
  9526. }
  9527. }
  9528.  
  9529. #main-canvas, #low-resolution-canvas {
  9530. opacity: 0.25;
  9531. }
  9532.  
  9533. #original-video-container {
  9534. video {
  9535. opacity: 0.15;
  9536. }
  9537. }
  9538.  
  9539. `;
  9540. static directions = {
  9541. d: "d",
  9542. a: "a",
  9543. right: "ArrowRight",
  9544. left: "ArrowLeft"
  9545. };
  9546. static preferences = {
  9547. showOnHover: "showImagesWhenHovering",
  9548. backgroundOpacity: "galleryBackgroundOpacity",
  9549. resolution: "galleryResolution",
  9550. enlargeOnClick: "enlargeOnClick",
  9551. videoVolume: "videoVolume",
  9552. videoMuted: "videoMuted"
  9553. };
  9554. static webWorkers = {
  9555. renderer:
  9556. `
  9557. /* eslint-disable prefer-template */
  9558. /**
  9559. * @param {Number} milliseconds
  9560. * @returns {Promise}
  9561. */
  9562. function sleep(milliseconds) {
  9563. return new Promise(resolve => setTimeout(resolve, milliseconds));
  9564. }
  9565.  
  9566. class RenderRequest {
  9567. /**
  9568. * @type {String}
  9569. */
  9570. id;
  9571. /**
  9572. * @type {String}
  9573. */
  9574. imageURL;
  9575. /**
  9576. * @type {String}
  9577. */
  9578. extension;
  9579. /**
  9580. * @type {String}
  9581. */
  9582. thumbURL;
  9583. /**
  9584. * @type {String}
  9585. */
  9586. fetchDelay;
  9587. /**
  9588. * @type {Number}
  9589. */
  9590. pixelCount;
  9591. /**
  9592. * @type {OffscreenCanvas}
  9593. */
  9594. canvas;
  9595. /**
  9596. * @type {Number}
  9597. */
  9598. resolutionFraction;
  9599. /**
  9600. * @type {AbortController}
  9601. */
  9602. abortController;
  9603. /**
  9604. * @type {Number}
  9605. */
  9606. get estimatedMegabyteSize() {
  9607. const rgb = 3;
  9608. const bytes = rgb * this.pixelCount;
  9609. const numberOfBytesInMegabyte = 1048576;
  9610. return bytes / numberOfBytesInMegabyte;
  9611. }
  9612.  
  9613. /**
  9614. * @param {{
  9615. * id: String,
  9616. * imageURL: String,
  9617. * extension: String,
  9618. * thumbURL: String,
  9619. * fetchDelay: String,
  9620. * pixelCount: Number,
  9621. * canvas: OffscreenCanvas,
  9622. * resolutionFraction: Number
  9623. * }} request
  9624. */
  9625. constructor(request) {
  9626. this.id = request.id;
  9627. this.imageURL = request.imageURL;
  9628. this.extension = request.extension;
  9629. this.thumbURL = request.thumbURL;
  9630. this.fetchDelay = request.fetchDelay;
  9631. this.pixelCount = request.pixelCount;
  9632. this.canvas = request.canvas;
  9633. this.resolutionFraction = request.resolutionFraction;
  9634. this.abortController = new AbortController();
  9635. }
  9636. }
  9637.  
  9638. class BatchRenderRequest {
  9639. static settings = {
  9640. megabyteMemoryLimit: 1000,
  9641. minimumRequestCount: 10
  9642. };
  9643.  
  9644. /**
  9645. * @type {String}
  9646. */
  9647. id;
  9648. /**
  9649. * @type {String}
  9650. */
  9651. requestType;
  9652. /**
  9653. * @type {RenderRequest[]}
  9654. */
  9655. renderRequests;
  9656. /**
  9657. * @type {RenderRequest[]}
  9658. */
  9659. originalRenderRequests;
  9660.  
  9661. get renderRequestIds() {
  9662. return new Set(this.renderRequests.map(request => request.id));
  9663. }
  9664.  
  9665. /**
  9666. * @param {{
  9667. * id: String,
  9668. * requestType: String,
  9669. * renderRequests: {
  9670. * id: String,
  9671. * imageURL: String,
  9672. * extension: String,
  9673. * thumbURL: String,
  9674. * fetchDelay: String,
  9675. * pixelCount: Number,
  9676. * canvas: OffscreenCanvas,
  9677. * resolutionFraction: Number
  9678. * }[]
  9679. * }} batchRequest
  9680. */
  9681. constructor(batchRequest) {
  9682. this.id = batchRequest.id;
  9683. this.requestType = batchRequest.requestType;
  9684. this.renderRequests = batchRequest.renderRequests.map(r => new RenderRequest(r));
  9685. this.originalRenderRequests = this.renderRequests;
  9686. this.truncateRenderRequestsExceedingMemoryLimit();
  9687. }
  9688.  
  9689. truncateRenderRequestsExceedingMemoryLimit() {
  9690. const truncatedRequests = [];
  9691. let currentMegabyteSize = 0;
  9692.  
  9693. for (const request of this.renderRequests) {
  9694. const overMemoryLimit = currentMegabyteSize < BatchRenderRequest.settings.megabyteMemoryLimit;
  9695. const underMinimumRequestCount = truncatedRequests.length < BatchRenderRequest.settings.minimumRequestCount;
  9696.  
  9697. if (overMemoryLimit || underMinimumRequestCount) {
  9698. truncatedRequests.push(request);
  9699. currentMegabyteSize += request.estimatedMegabyteSize;
  9700. } else {
  9701. postMessage({
  9702. action: "renderDeleted",
  9703. id: request.id
  9704. });
  9705. }
  9706. }
  9707. this.renderRequests = truncatedRequests;
  9708. }
  9709. }
  9710.  
  9711. class ImageFetcher {
  9712. /**
  9713. * @type {Set.<String>}
  9714. */
  9715. static idsToFetchFromPostPages = new Set();
  9716.  
  9717. /**
  9718. * @type {Number}
  9719. */
  9720. static get postPageFetchDelay() {
  9721. return ImageFetcher.idsToFetchFromPostPages.size * 250;
  9722. }
  9723.  
  9724. /**
  9725. * @param {RenderRequest} request
  9726. */
  9727. static async setOriginalImageURLAndExtension(request) {
  9728. if (request.extension !== null && request.extension !== undefined) {
  9729. request.imageURL = request.imageURL.replace("jpg", request.extension);
  9730. } else {
  9731. // eslint-disable-next-line require-atomic-updates
  9732. request.imageURL = await ImageFetcher.getOriginalImageURL(request.id);
  9733. request.extension = ImageFetcher.getExtensionFromImageURL(request.imageURL);
  9734. }
  9735. }
  9736.  
  9737. /**
  9738. * @param {String} id
  9739. * @returns {String}
  9740. */
  9741. static getOriginalImageURL(id) {
  9742. const apiURL = "https://api.rule34.xxx//index.php?page=dapi&s=post&q=index&id=" + id;
  9743. return fetch(apiURL)
  9744. .then((response) => {
  9745. if (response.ok) {
  9746. return response.text();
  9747. }
  9748. throw new Error(response.status + ": " + id);
  9749. })
  9750. .then((html) => {
  9751. return (/ file_url="(.*?)"/).exec(html)[1].replace("api-cdn.", "");
  9752. }).catch(() => {
  9753. return ImageFetcher.getOriginalImageURLFromPostPage(id);
  9754. });
  9755. }
  9756.  
  9757. /**
  9758. * @param {String} id
  9759. * @returns {String}
  9760. */
  9761. static async getOriginalImageURLFromPostPage(id) {
  9762. const postPageURL = "https://rule34.xxx/index.php?page=post&s=view&id=" + id;
  9763.  
  9764. ImageFetcher.idsToFetchFromPostPages.add(id);
  9765. await sleep(ImageFetcher.postPageFetchDelay);
  9766. return fetch(postPageURL)
  9767. .then((response) => {
  9768. if (response.ok) {
  9769. return response.text();
  9770. }
  9771. throw new Error(response.status + ": " + postPageURL);
  9772. })
  9773. .then((html) => {
  9774. ImageFetcher.idsToFetchFromPostPages.delete(id);
  9775. return (/itemprop="image" content="(.*)"/g).exec(html)[1].replace("us.rule34", "rule34");
  9776. }).catch((error) => {
  9777. if (error.message.includes("503")) {
  9778. return ImageFetcher.getOriginalImageURLFromPostPage(id);
  9779. }
  9780. console.error({
  9781. error,
  9782. url: postPageURL
  9783. });
  9784. return "https://rule34.xxx/images/r34chibi.png";
  9785. });
  9786. }
  9787.  
  9788. /**
  9789. * @param {String} imageURL
  9790. * @returns {String}
  9791. */
  9792. static getExtensionFromImageURL(imageURL) {
  9793. try {
  9794. return (/\.(png|jpg|jpeg|gif)/g).exec(imageURL)[1];
  9795. } catch (error) {
  9796. return "jpg";
  9797. }
  9798. }
  9799.  
  9800. /**
  9801. * @param {RenderRequest} request
  9802. * @returns {Promise}
  9803. */
  9804. static fetchImage(request) {
  9805. return fetch(request.imageURL.replace("wimg.", ""), {
  9806. signal: request.abortController.signal
  9807. });
  9808. }
  9809.  
  9810. /**
  9811. * @param {RenderRequest} request
  9812. * @returns {Blob}
  9813. */
  9814. static async fetchImageBlob(request) {
  9815. const response = await ImageFetcher.fetchImage(request);
  9816. return response.blob();
  9817. }
  9818.  
  9819. /**
  9820. * @param {String} id
  9821. * @returns {String}
  9822. */
  9823. static async findImageExtensionFromId(id) {
  9824. const imageURL = await ImageFetcher.getOriginalImageURL(id);
  9825. const extension = ImageFetcher.getExtensionFromImageURL(imageURL);
  9826.  
  9827. postMessage({
  9828. action: "extensionFound",
  9829. id,
  9830. extension
  9831. });
  9832. }
  9833. }
  9834.  
  9835. class ThumbUpscaler {
  9836. static settings = {
  9837. maxCanvasHeight: 16000
  9838. };
  9839. /**
  9840. * @type {Map.<String, OffscreenCanvas>}
  9841. */
  9842. canvases = new Map();
  9843. /**
  9844. * @type {Number}
  9845. */
  9846. screenWidth;
  9847. /**
  9848. * @type {Boolean}
  9849. */
  9850. onSearchPage;
  9851.  
  9852. /**
  9853. * @param {Number} screenWidth
  9854. * @param {Boolean} onSearchPage
  9855. */
  9856. constructor(screenWidth, onSearchPage) {
  9857. this.screenWidth = screenWidth;
  9858. this.onSearchPage = onSearchPage;
  9859. }
  9860.  
  9861. /**
  9862. * @param {{id: String, imageURL: String, canvas: OffscreenCanvas, resolutionFraction: Number}[]} message
  9863. */
  9864. async upscaleMultipleAnimatedCanvases(message) {
  9865. const requests = message.map(r => new RenderRequest(r));
  9866.  
  9867. requests.forEach((request) => {
  9868. this.collectCanvas(request);
  9869. });
  9870.  
  9871. for (const request of requests) {
  9872. ImageFetcher.fetchImage(request)
  9873. .then((response) => {
  9874. return response.blob();
  9875. })
  9876. .then((blob) => {
  9877. createImageBitmap(blob)
  9878. .then((imageBitmap) => {
  9879. this.upscale(request, imageBitmap);
  9880. });
  9881. });
  9882. await sleep(50);
  9883. }
  9884. }
  9885.  
  9886. /**
  9887. * @param {RenderRequest} request
  9888. * @param {ImageBitmap} imageBitmap
  9889. */
  9890. upscale(request, imageBitmap) {
  9891. if (this.onSearchPage || imageBitmap === undefined || !this.canvases.has(request.id)) {
  9892. return;
  9893. }
  9894. this.setCanvasDimensions(request, imageBitmap);
  9895. this.drawCanvas(request.id, imageBitmap);
  9896. }
  9897.  
  9898. /**
  9899. * @param {RenderRequest} request
  9900. * @param {ImageBitmap} imageBitmap
  9901. */
  9902. setCanvasDimensions(request, imageBitmap) {
  9903. const canvas = this.canvases.get(request.id);
  9904. let width = this.screenWidth / request.resolutionFraction;
  9905. let height = (width / imageBitmap.width) * imageBitmap.height;
  9906.  
  9907. if (width > imageBitmap.width) {
  9908. width = imageBitmap.width;
  9909. height = imageBitmap.height;
  9910. }
  9911.  
  9912. if (height > ThumbUpscaler.settings.maxCanvasHeight) {
  9913. width *= (ThumbUpscaler.settings.maxCanvasHeight / height);
  9914. height = ThumbUpscaler.settings.maxCanvasHeight;
  9915. }
  9916. canvas.width = width;
  9917. canvas.height = height;
  9918. }
  9919.  
  9920. /**
  9921. * @param {String} id
  9922. * @param {ImageBitmap} imageBitmap
  9923. */
  9924. drawCanvas(id, imageBitmap) {
  9925. const canvas = this.canvases.get(id);
  9926. const context = canvas.getContext("2d");
  9927.  
  9928. context.clearRect(0, 0, canvas.width, canvas.height);
  9929. context.drawImage(
  9930. imageBitmap, 0, 0, imageBitmap.width, imageBitmap.height,
  9931. 0, 0, canvas.width, canvas.height
  9932. );
  9933. }
  9934.  
  9935. deleteAllCanvases() {
  9936. for (const [id, canvas] of this.canvases.entries()) {
  9937. this.deleteCanvas(id, canvas);
  9938. }
  9939. this.canvases.clear();
  9940. }
  9941.  
  9942. /**
  9943. * @param {String} id
  9944. * @param {OffscreenCanvas} canvas
  9945. */
  9946. deleteCanvas(id, canvas) {
  9947. const context = canvas.getContext("2d");
  9948.  
  9949. context.clearRect(0, 0, canvas.width, canvas.height);
  9950. canvas.width = 0;
  9951. canvas.height = 0;
  9952. canvas = null;
  9953. this.canvases.set(id, canvas);
  9954. this.canvases.delete(id);
  9955. }
  9956.  
  9957. /**
  9958. * @param {RenderRequest} request
  9959. */
  9960. collectCanvas(request) {
  9961. if (request.canvas === undefined) {
  9962. return;
  9963. }
  9964.  
  9965. if (!this.canvases.has(request.id)) {
  9966. this.canvases.set(request.id, request.canvas);
  9967. }
  9968. }
  9969.  
  9970. /**
  9971. * @param {BatchRenderRequest} batchRequest
  9972. */
  9973. collectCanvases(batchRequest) {
  9974. batchRequest.originalRenderRequests.forEach((request) => {
  9975. this.collectCanvas(request);
  9976. });
  9977. }
  9978. }
  9979.  
  9980. class ImageRenderer {
  9981. /**
  9982. * @type {OffscreenCanvas}
  9983. */
  9984. canvas;
  9985. /**
  9986. * @type {CanvasRenderingContext2D}
  9987. */
  9988. context;
  9989. /**
  9990. * @type {ThumbUpscaler}
  9991. */
  9992. thumbUpscaler;
  9993. /**
  9994. * @type {RenderRequest}
  9995. */
  9996. renderRequest;
  9997. /**
  9998. * @type {BatchRenderRequest}
  9999. */
  10000. batchRenderRequest;
  10001. /**
  10002. * @type {Map.<String, RenderRequest>}
  10003. */
  10004. incompleteRenderRequests;
  10005. /**
  10006. * @type {Map.<String, {completed: Boolean, imageBitmap: ImageBitmap, request: RenderRequest}>}
  10007. */
  10008. renders;
  10009. /**
  10010. * @type {String}
  10011. */
  10012. lastRequestedDrawId;
  10013. /**
  10014. * @type {String}
  10015. */
  10016. currentlyDrawnId;
  10017. /**
  10018. * @type {Boolean}
  10019. */
  10020. onMobileDevice;
  10021. /**
  10022. * @type {Boolean}
  10023. */
  10024. onSearchPage;
  10025. /**
  10026. * @type {Boolean}
  10027. */
  10028. usingLandscapeOrientation;
  10029.  
  10030. /**
  10031. * @type {Boolean}
  10032. */
  10033. get hasRenderRequest() {
  10034. return this.renderRequest !== undefined &&
  10035. this.renderRequest !== null;
  10036. }
  10037.  
  10038. /**
  10039. * @type {Boolean}
  10040. */
  10041. get hasBatchRenderRequest() {
  10042. return this.batchRenderRequest !== undefined &&
  10043. this.batchRenderRequest !== null;
  10044. }
  10045.  
  10046. /**
  10047. * @param {{canvas: OffscreenCanvas, screenWidth: Number, onMobileDevice: Boolean, onSearchPage: Boolean }} message
  10048. */
  10049. constructor(message) {
  10050. this.canvas = message.canvas;
  10051. this.context = this.canvas.getContext("2d");
  10052. this.thumbUpscaler = new ThumbUpscaler(message.screenWidth, message.onSearchPage);
  10053. this.renders = new Map();
  10054. this.incompleteRenderRequests = new Map();
  10055. this.lastRequestedDrawId = "";
  10056. this.currentlyDrawnId = "";
  10057. this.onMobileDevice = message.onMobileDevice;
  10058. this.onSearchPage = message.onSearchPage;
  10059. this.usingLandscapeOrientation = true;
  10060. this.configureCanvasQuality();
  10061. }
  10062.  
  10063. configureCanvasQuality() {
  10064. this.context.imageSmoothingEnabled = true;
  10065. this.context.imageSmoothingQuality = "high";
  10066. this.context.lineJoin = "miter";
  10067. }
  10068.  
  10069. renderMultipleImages(message) {
  10070. const batchRenderRequest = new BatchRenderRequest(message);
  10071.  
  10072. this.thumbUpscaler.collectCanvases(batchRenderRequest);
  10073. this.abortOutdatedFetchRequests(batchRenderRequest);
  10074. this.deleteRendersNotInNewRequests(batchRenderRequest);
  10075. this.removeStartedRenderRequests(batchRenderRequest);
  10076. this.batchRenderRequest = batchRenderRequest;
  10077. this.renderMultipleImagesHelper(batchRenderRequest);
  10078. }
  10079.  
  10080. /**
  10081. * @param {BatchRenderRequest} batchRenderRequest
  10082. */
  10083. async renderMultipleImagesHelper(batchRenderRequest) {
  10084. for (const request of batchRenderRequest.renderRequests) {
  10085. if (this.renders.has(request.id)) {
  10086. continue;
  10087. }
  10088. this.renders.set(request.id, {
  10089. completed: false,
  10090. imageBitmap: undefined,
  10091. request
  10092. });
  10093. }
  10094.  
  10095. for (const request of batchRenderRequest.renderRequests) {
  10096. this.renderImage(request);
  10097. await sleep(request.fetchDelay);
  10098. }
  10099. }
  10100.  
  10101. /**
  10102. * @param {RenderRequest} request
  10103. * @param {Number} batchRequestId
  10104. */
  10105. async renderImage(request) {
  10106. this.incompleteRenderRequests.set(request.id, request);
  10107. await ImageFetcher.setOriginalImageURLAndExtension(request);
  10108. let blob;
  10109.  
  10110. try {
  10111. blob = await ImageFetcher.fetchImageBlob(request);
  10112. } catch (error) {
  10113. if (error.name === "AbortError") {
  10114. this.deleteRender(request.id);
  10115. } else {
  10116. console.error({
  10117. error,
  10118. request
  10119. });
  10120. }
  10121. return;
  10122. }
  10123. const imageBitmap = await createImageBitmap(blob);
  10124.  
  10125. this.renders.set(request.id, {
  10126. completed: true,
  10127. imageBitmap,
  10128. request
  10129. });
  10130. this.incompleteRenderRequests.delete(request.id);
  10131. this.thumbUpscaler.upscale(request, imageBitmap);
  10132. postMessage({
  10133. action: "renderCompleted",
  10134. extension: request.extension,
  10135. id: request.id
  10136. });
  10137.  
  10138. if (this.lastRequestedDrawId === request.id) {
  10139. this.drawCanvas(request.id);
  10140. }
  10141. }
  10142.  
  10143. /**
  10144. * @param {String} id
  10145. * @returns {Boolean}
  10146. */
  10147. renderHasCompleted(id) {
  10148. const render = this.renders.get(id);
  10149. return render !== undefined && render.completed;
  10150. }
  10151.  
  10152. /**
  10153. * @param {String} id
  10154. */
  10155. drawCanvas(id) {
  10156. const render = this.renders.get(id);
  10157.  
  10158. if (render === undefined || render.imageBitmap === undefined) {
  10159. this.clearCanvas();
  10160. return;
  10161. }
  10162.  
  10163. if (this.currentlyDrawnId === id) {
  10164. return;
  10165. }
  10166.  
  10167. if (render.completed) {
  10168. this.currentlyDrawnCanvasId = id;
  10169. }
  10170. const ratio = Math.min(this.canvas.width / render.imageBitmap.width, this.canvas.height / render.imageBitmap.height);
  10171. const centerShiftX = (this.canvas.width - (render.imageBitmap.width * ratio)) / 2;
  10172. const centerShiftY = (this.canvas.height - (render.imageBitmap.height * ratio)) / 2;
  10173.  
  10174. this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
  10175. this.context.drawImage(
  10176. render.imageBitmap, 0, 0, render.imageBitmap.width, render.imageBitmap.height,
  10177. centerShiftX, centerShiftY, render.imageBitmap.width * ratio, render.imageBitmap.height * ratio
  10178. );
  10179. }
  10180.  
  10181. /**
  10182. * @param {Boolean} usingLandscapeOrientation
  10183. */
  10184. changeCanvasOrientation(usingLandscapeOrientation) {
  10185. if (usingLandscapeOrientation !== this.usingLandscapeOrientation) {
  10186. this.swapCanvasOrientation();
  10187. }
  10188. }
  10189.  
  10190. swapCanvasOrientation() {
  10191. const temp = this.canvas.width;
  10192.  
  10193. this.canvas.width = this.canvas.height;
  10194. this.canvas.height = temp;
  10195. this.usingLandscapeOrientation = !this.usingLandscapeOrientation;
  10196. }
  10197.  
  10198. clearCanvas() {
  10199. this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
  10200. }
  10201.  
  10202. deleteAllRenders() {
  10203. this.thumbUpscaler.deleteAllCanvases();
  10204. this.abortAllFetchRequests();
  10205.  
  10206. for (const id of this.renders.keys()) {
  10207. this.deleteRender(id, true);
  10208. }
  10209. this.batchRenderRequest = undefined;
  10210. this.renderRequest = undefined;
  10211. this.renders.clear();
  10212. }
  10213.  
  10214. /**
  10215. * @param {BatchRenderRequest} newBatchRenderRequest
  10216. */
  10217. deleteRendersNotInNewRequests(newBatchRenderRequest) {
  10218. const idsToRender = newBatchRenderRequest.renderRequestIds;
  10219.  
  10220. for (const id of this.renders.keys()) {
  10221. if (!idsToRender.has(id)) {
  10222. this.deleteRender(id);
  10223. }
  10224. }
  10225. }
  10226.  
  10227. /**
  10228. * @param {String} id
  10229. * @param {Boolean} initiatedByMainThread
  10230. */
  10231. deleteRender(id, initiatedByMainThread = false) {
  10232. if (!this.renders.has(id)) {
  10233. return;
  10234. }
  10235. const imageBitmap = this.renders.get(id).imageBitmap;
  10236.  
  10237. if (imageBitmap !== null && imageBitmap !== undefined) {
  10238. imageBitmap.close();
  10239. }
  10240. this.renders.set(id, null);
  10241. this.renders.delete(id);
  10242.  
  10243. if (initiatedByMainThread) {
  10244. return;
  10245. }
  10246. postMessage({
  10247. action: "renderDeleted",
  10248. id
  10249. });
  10250. }
  10251.  
  10252. /**
  10253. * @param {BatchRenderRequest} newBatchRenderRequest
  10254. */
  10255. abortOutdatedFetchRequests(newBatchRenderRequest) {
  10256. const newIds = newBatchRenderRequest.renderRequestIds;
  10257.  
  10258. for (const [id, request] of this.incompleteRenderRequests.entries()) {
  10259. if (!newIds.has(id)) {
  10260. request.abortController.abort();
  10261. this.incompleteRenderRequests.delete(id);
  10262. }
  10263. }
  10264. }
  10265.  
  10266. abortAllFetchRequests() {
  10267. for (const request of this.incompleteRenderRequests.values()) {
  10268. request.abortController.abort();
  10269. }
  10270. this.incompleteRenderRequests.clear();
  10271. }
  10272.  
  10273. /**
  10274. * @param {BatchRenderRequest} batchRenderRequest
  10275. */
  10276. removeStartedRenderRequests(batchRenderRequest) {
  10277. batchRenderRequest.renderRequests = batchRenderRequest.renderRequests
  10278. .filter(request => !this.renders.has(request.id));
  10279. }
  10280. /**
  10281. * @param {BatchRenderRequest} batchRenderRequest
  10282. */
  10283. removeCompletedRenderRequests(batchRenderRequest) {
  10284. batchRenderRequest.renderRequests = batchRenderRequest.renderRequests
  10285. .filter(request => !this.renderHasCompleted(request.id));
  10286. }
  10287.  
  10288. upscaleAllRenderedThumbs() {
  10289. for (const render of this.renders.values()) {
  10290. this.thumbUpscaler.upscale(render.request, render.imageBitmap);
  10291. }
  10292. }
  10293.  
  10294. onmessage(message) {
  10295. switch (message.action) {
  10296. case "render":
  10297. this.renderRequest = new RenderRequest(message);
  10298. this.lastRequestedDrawId = message.id;
  10299. this.thumbUpscaler.collectCanvas(this.renderRequest);
  10300. this.renderImage(this.renderRequest);
  10301. break;
  10302.  
  10303. case "renderMultiple":
  10304. this.renderMultipleImages(message);
  10305. break;
  10306.  
  10307. case "deleteAllRenders":
  10308. this.deleteAllRenders();
  10309. break;
  10310.  
  10311. case "drawMainCanvas":
  10312. this.lastRequestedDrawId = message.id;
  10313. this.drawCanvas(message.id);
  10314. break;
  10315.  
  10316. case "clearMainCanvas":
  10317. this.clearCanvas();
  10318. break;
  10319.  
  10320. case "upscaleAnimatedThumbs":
  10321. this.thumbUpscaler.upscaleMultipleAnimatedCanvases(message.upscaleRequests);
  10322. break;
  10323.  
  10324. case "changeCanvasOrientation":
  10325. this.changeCanvasOrientation(message.usingLandscapeOrientation);
  10326. break;
  10327.  
  10328. case "upscaleAllRenderedThumbs":
  10329. this.upscaleAllRenderedThumbs();
  10330. break;
  10331.  
  10332. default:
  10333. break;
  10334. }
  10335. }
  10336. }
  10337.  
  10338. /**
  10339. * @type {ImageRenderer}
  10340. */
  10341. let imageRenderer;
  10342.  
  10343. onmessage = (message) => {
  10344. switch (message.data.action) {
  10345. case "initialize":
  10346. BatchRenderRequest.settings.megabyteMemoryLimit = message.data.megabyteLimit;
  10347. BatchRenderRequest.settings.minimumRequestCount = message.data.minimumImagesToRender;
  10348. imageRenderer = new ImageRenderer(message.data);
  10349. break;
  10350.  
  10351. case "findExtension":
  10352. ImageFetcher.findImageExtensionFromId(message.data.id);
  10353. break;
  10354.  
  10355. default:
  10356. imageRenderer.onmessage(message.data);
  10357. break;
  10358. }
  10359. };
  10360.  
  10361. `
  10362. };
  10363. static canvasResolutions = {
  10364. search: "3840x2160",
  10365. favorites: Utils.onMobileDevice() ? "1920x1080" : "7680x4320",
  10366. low: Utils.onMobileDevice() ? "640x360" : "1280:720"
  10367. };
  10368. static swipeControls = {
  10369. threshold: 60,
  10370. touchStart: {
  10371. x: 0,
  10372. y: 0
  10373. },
  10374. touchEnd: {
  10375. x: 0,
  10376. y: 0
  10377. },
  10378. get deltaX() {
  10379. return this.touchStart.x - this.touchEnd.x;
  10380. },
  10381. get deltaY() {
  10382. return this.touchStart.y - this.touchEnd.y;
  10383. },
  10384. get right() {
  10385. return this.deltaX < -this.threshold;
  10386. },
  10387. get left() {
  10388. return this.deltaX > this.threshold;
  10389. },
  10390. get up() {
  10391. return this.deltaY > this.threshold;
  10392. },
  10393. get down() {
  10394. return this.deltaY < -this.threshold;
  10395. },
  10396. /**
  10397. * @param {TouchEvent} touchEvent
  10398. * @param {Boolean} atStart
  10399. */
  10400. set(touchEvent, atStart) {
  10401. if (atStart) {
  10402. this.touchStart.x = touchEvent.changedTouches[0].screenX;
  10403. this.touchStart.y = touchEvent.changedTouches[0].screenY;
  10404. } else {
  10405. this.touchEnd.x = touchEvent.changedTouches[0].screenX;
  10406. this.touchEnd.y = touchEvent.changedTouches[0].screenY;
  10407. }
  10408. }
  10409. };
  10410. static commonVideoAttributes = "width=\"100%\" height=\"100%\" autoplay muted loop controlsList=\"nofullscreen\" webkit-playsinline playsinline";
  10411. static settings = {
  10412. maxImagesToRenderInBackground: 50,
  10413. maxImagesToRenderAround: Utils.onMobileDevice() ? 3 : 50,
  10414. megabyteLimit: Utils.onMobileDevice() ? 0 : 400,
  10415. minImagesToRender: Utils.onMobileDevice() ? 3 : 8,
  10416. imageFetchDelay: 250,
  10417. throttledImageFetchDelay: 400,
  10418. imageFetchDelayWhenExtensionKnown: Utils.onMobileDevice() ? 50 : 25,
  10419. upscaledThumbResolutionFraction: 4,
  10420. upscaledAnimatedThumbResolutionFraction: 6,
  10421. animatedThumbsToUpscaleRange: 20,
  10422. animatedThumbsToUpscaleDiscrete: 20,
  10423. traversalCooldownTime: 300,
  10424. renderOnPageChangeCooldownTime: 2000,
  10425. addFavoriteCooldownTime: 250,
  10426. cursorVisibilityCooldownTime: 500,
  10427. imageExtensionAssignmentCooldownTime: 1000,
  10428. additionalVideoPlayerCount: Utils.onMobileDevice() ? 2 : 2,
  10429. renderAroundAggressively: true,
  10430. loopAtEndOfGalleryValue: false,
  10431. get loopAtEndOfGallery() {
  10432. if (!Utils.onFavoritesPage() || !Gallery.finishedLoading) {
  10433. return true;
  10434. }
  10435. return this.loopAtEndOfGalleryValue;
  10436. },
  10437. debugEnabled: false
  10438. };
  10439. static keyHeldDownTraversalCooldown = new Cooldown(Gallery.settings.traversalCooldownTime);
  10440. static backgroundRenderingOnPageChangeCooldown = new Cooldown(Gallery.settings.renderOnPageChangeCooldownTime, true);
  10441. static addOrRemoveFavoriteCooldown = new Cooldown(Gallery.settings.addFavoriteCooldownTime, true);
  10442. static cursorVisibilityCooldown = new Cooldown(Gallery.settings.cursorVisibilityCooldownTime);
  10443. static finishedLoading = Utils.onSearchPage();
  10444. /**
  10445. * @returns {Boolean}
  10446. */
  10447. static get disabled() {
  10448. return (Utils.onMobileDevice() && Utils.onSearchPage()) || Utils.getPerformanceProfile() > 0 || Utils.onPostPage();
  10449. }
  10450.  
  10451. /**
  10452. * @type {Autoplay}
  10453. */
  10454. autoplayController;
  10455. /**
  10456. * @type {HTMLDivElement}
  10457. */
  10458. originalContentContainer;
  10459. /**
  10460. * @type {HTMLCanvasElement}
  10461. */
  10462. mainCanvas;
  10463. /**
  10464. * @type {HTMLCanvasElement}
  10465. */
  10466. lowResolutionCanvas;
  10467. /**
  10468. * @type {CanvasRenderingContext2D}
  10469. */
  10470. lowResolutionContext;
  10471. /**
  10472. * @type {HTMLAnchorElement}
  10473. */
  10474. videoContainer;
  10475. /**
  10476. * @type {HTMLVideoElement[]}
  10477. */
  10478. videoPlayers;
  10479. /**
  10480. * @type {HTMLImageElement}
  10481. */
  10482. gifContainer;
  10483. /**
  10484. * @type {HTMLDivElement}
  10485. */
  10486. originalImageLinkMask;
  10487. /**
  10488. * @type {HTMLAnchorElement}
  10489. */
  10490. background;
  10491. /**
  10492. * @type {HTMLElement}
  10493. */
  10494. thumbUnderCursor;
  10495. /**
  10496. * @type {HTMLElement}
  10497. */
  10498. lastEnteredThumb;
  10499. /**
  10500. * @type {Worker}
  10501. */
  10502. imageRenderer;
  10503. /**
  10504. * @type {Set.<String>}
  10505. */
  10506. startedRenders;
  10507. /**
  10508. * @type {Set.<String>}
  10509. */
  10510. completedRenders;
  10511. /**
  10512. * @type {Map.<String, HTMLCanvasElement>}
  10513. */
  10514. transferredCanvases;
  10515. /**
  10516. * @type {Map.<String, VideoClip>}
  10517. */
  10518. videoClips;
  10519. /**
  10520. * @type {Map.<String, String>}
  10521. */
  10522. enumeratedThumbs;
  10523. /**
  10524. * @type {HTMLElement[]}
  10525. */
  10526. visibleThumbs;
  10527. /**
  10528. * @type {Post[]}
  10529. */
  10530. latestSearchResults;
  10531. /**
  10532. * @type {Object.<Number, String>}
  10533. */
  10534. imageExtensions;
  10535. /**
  10536. * @type {String}
  10537. */
  10538. foundFavoriteId;
  10539. /**
  10540. * @type {String}
  10541. */
  10542. changedPageInGalleryDirection;
  10543. /**
  10544. * @type {Number}
  10545. */
  10546. recentlyDiscoveredImageExtensionCount;
  10547. /**
  10548. * @type {Number}
  10549. */
  10550. currentlySelectedThumbIndex;
  10551. /**
  10552. * @type {Number}
  10553. */
  10554. lastSelectedThumbIndexBeforeEnteringGallery;
  10555. /**
  10556. * @type {Number}
  10557. */
  10558. currentBatchRenderRequestId;
  10559. /**
  10560. * @type {Boolean}
  10561. */
  10562. inGallery;
  10563. /**
  10564. * @type {Boolean}
  10565. */
  10566. recentlyEnteredGallery;
  10567. /**
  10568. * @type {Boolean}
  10569. */
  10570. recentlyExitedGallery;
  10571. /**
  10572. * @type {Boolean}
  10573. */
  10574. leftPage;
  10575. /**
  10576. * @type {Boolean}
  10577. */
  10578. favoritesWereFetched;
  10579. /**
  10580. * @type {Boolean}
  10581. */
  10582. showOriginalContentOnHover;
  10583. /**
  10584. * @type {Boolean}
  10585. */
  10586. enlargeOnClickOnMobile;
  10587.  
  10588. /**
  10589. * @type {Boolean}
  10590. */
  10591. get changedPageWhileInGallery() {
  10592. return this.changedPageInGalleryDirection !== null;
  10593. }
  10594.  
  10595. constructor() {
  10596. if (Gallery.disabled) {
  10597. return;
  10598. }
  10599. this.createAutoplayController();
  10600. this.initializeFields();
  10601. this.initializeTimers();
  10602. this.setMainCanvasResolution();
  10603. this.createWebWorkers();
  10604. this.createVideoBackgrounds();
  10605. this.addEventListeners();
  10606. this.createImageRendererMessageHandler();
  10607. this.prepareSearchPage();
  10608. this.insertHTML();
  10609. this.updateBackgroundOpacity(Utils.getPreference(Gallery.preferences.backgroundOpacity, 1));
  10610. this.loadVideoClips();
  10611. this.setOrientation();
  10612. this.createMobileTapControls();
  10613. }
  10614.  
  10615. initializeFields() {
  10616. this.mainCanvas = document.createElement("canvas");
  10617. this.lowResolutionCanvas = document.createElement("canvas");
  10618. this.lowResolutionContext = this.lowResolutionCanvas.getContext("2d");
  10619. this.thumbUnderCursor = null;
  10620. this.lastEnteredThumb = null;
  10621. this.startedRenders = new Set();
  10622. this.completedRenders = new Set();
  10623. this.transferredCanvases = new Map();
  10624. this.videoClips = new Map();
  10625. this.enumeratedThumbs = new Map();
  10626. this.visibleThumbs = [];
  10627. this.latestSearchResults = [];
  10628. this.imageExtensions = {};
  10629. this.foundFavoriteId = null;
  10630. this.changedPageInGalleryDirection = null;
  10631. this.recentlyDiscoveredImageExtensionCount = 0;
  10632. this.currentlySelectedThumbIndex = 0;
  10633. this.lastSelectedThumbIndexBeforeEnteringGallery = 0;
  10634. this.currentBatchRenderRequestId = 0;
  10635. this.inGallery = false;
  10636. this.recentlyEnteredGallery = false;
  10637. this.recentlyExitedGallery = false;
  10638. this.leftPage = false;
  10639. this.favoritesWereFetched = false;
  10640. this.showOriginalContentOnHover = Utils.getPreference(Gallery.preferences.showOnHover, true);
  10641. this.enlargeOnClickOnMobile = Utils.getPreference(Gallery.preferences.enlargeOnClick, true);
  10642. }
  10643.  
  10644. initializeTimers() {
  10645. Gallery.backgroundRenderingOnPageChangeCooldown.onDebounceEnd = () => {
  10646. this.onPageChange();
  10647. };
  10648. }
  10649.  
  10650. setMainCanvasResolution() {
  10651. const resolution = Utils.onSearchPage() ? Gallery.canvasResolutions.search : Gallery.canvasResolutions.favorites;
  10652. const dimensions = resolution.split("x").map(dimension => parseFloat(dimension));
  10653.  
  10654. this.mainCanvas.width = dimensions[0];
  10655. this.mainCanvas.height = dimensions[1];
  10656. }
  10657.  
  10658. createWebWorkers() {
  10659. const offscreenCanvas = this.mainCanvas.transferControlToOffscreen();
  10660.  
  10661. this.imageRenderer = new Worker(Utils.getWorkerURL(Gallery.webWorkers.renderer));
  10662. this.imageRenderer.postMessage({
  10663. action: "initialize",
  10664. canvas: offscreenCanvas,
  10665. onMobileDevice: Utils.onMobileDevice(),
  10666. screenWidth: window.screen.width,
  10667. megabyteLimit: Gallery.settings.megabyteLimit,
  10668. minimumImagesToRender: Gallery.settings.minImagesToRender,
  10669. onSearchPage: Utils.onSearchPage()
  10670. }, [offscreenCanvas]);
  10671. }
  10672.  
  10673. createVideoBackgrounds() {
  10674. document.createElement("canvas").toBlob((blob) => {
  10675. const videoBackgroundURL = URL.createObjectURL(blob);
  10676.  
  10677. for (const video of this.videoPlayers) {
  10678. video.setAttribute("poster", videoBackgroundURL);
  10679. }
  10680. });
  10681. }
  10682.  
  10683. addEventListeners() {
  10684. this.addGalleryEventListeners();
  10685. this.addFavoritesLoaderEventListeners();
  10686. this.addMobileEventListeners();
  10687. this.addMemoryManagementEventListeners();
  10688. }
  10689.  
  10690. addGalleryEventListeners() {
  10691. window.addEventListener("load", () => {
  10692. if (Utils.onSearchPage()) {
  10693. this.initializeThumbsForHovering.bind(this)();
  10694. this.enumerateThumbs();
  10695. }
  10696. this.hideCaptionsWhenShowingOriginalContent();
  10697. }, {
  10698. once: true,
  10699. passive: true
  10700. });
  10701.  
  10702. // eslint-disable-next-line complexity
  10703. document.addEventListener("mousedown", (event) => {
  10704. if (this.clickedOnAutoplayMenu(event)) {
  10705. return;
  10706. }
  10707. const clickedOnTapControls = event.target.classList.contains("mobile-tap-control");
  10708.  
  10709. if (clickedOnTapControls) {
  10710. return;
  10711. }
  10712. const clickedOnAnImage = event.target.tagName.toLowerCase() === "img" && !event.target.parentElement.classList.contains("add-or-remove-button");
  10713. const clickedOnAThumb = clickedOnAnImage && (Utils.getThumbFromImage(event.target).className.includes("thumb") || Utils.getThumbFromImage(event.target).className.includes(Utils.favoriteItemClassName));
  10714. const clickedOnACaptionTag = event.target.classList.contains("caption-tag");
  10715. const thumb = clickedOnAThumb ? Utils.getThumbFromImage(event.target) : null;
  10716.  
  10717. if (clickedOnAThumb) {
  10718. this.currentlySelectedThumbIndex = this.getIndexFromThumb(thumb);
  10719. }
  10720.  
  10721. if (event.ctrlKey && event.button === Utils.clickCodes.left) {
  10722. return;
  10723. }
  10724.  
  10725. switch (event.button) {
  10726. case Utils.clickCodes.left:
  10727. if (event.shiftKey && (this.inGallery || clickedOnAThumb)) {
  10728. this.openPostInNewPage();
  10729. return;
  10730. }
  10731.  
  10732. if (this.inGallery) {
  10733. if (Utils.isVideo(this.getSelectedThumb()) && !Utils.onMobileDevice()) {
  10734. return;
  10735. }
  10736. this.exitGallery();
  10737. this.toggleAllVisibility(false);
  10738. return;
  10739. }
  10740.  
  10741. if (!clickedOnAThumb) {
  10742. return;
  10743. }
  10744.  
  10745. if (Utils.onMobileDevice()) {
  10746. if (!this.enlargeOnClickOnMobile) {
  10747. this.openPostInNewPage(thumb);
  10748. return;
  10749. }
  10750. this.deleteAllRenders();
  10751. }
  10752.  
  10753. if (Utils.onMobileDevice()) {
  10754. this.renderImagesAround(thumb);
  10755. }
  10756.  
  10757. this.toggleAllVisibility(true);
  10758. this.enterGallery();
  10759. this.showOriginalContent(thumb);
  10760. break;
  10761.  
  10762. case Utils.clickCodes.middle:
  10763. event.preventDefault();
  10764.  
  10765. if (this.inGallery || (clickedOnAThumb && Utils.onSearchPage())) {
  10766. this.openPostInNewPage();
  10767. return;
  10768. }
  10769.  
  10770. if (!clickedOnAThumb && !clickedOnACaptionTag) {
  10771. this.toggleAllVisibility();
  10772. Utils.setPreference(Gallery.preferences.showOnHover, this.showOriginalContentOnHover);
  10773. }
  10774. break;
  10775.  
  10776. default:
  10777. break;
  10778. }
  10779. });
  10780. window.addEventListener("auxclick", (event) => {
  10781. if (event.button === Utils.clickCodes.middle) {
  10782. event.preventDefault();
  10783. }
  10784. });
  10785. document.addEventListener("wheel", (event) => {
  10786. if (event.shiftKey) {
  10787. return;
  10788. }
  10789.  
  10790. if (this.inGallery) {
  10791. if (event.ctrlKey) {
  10792. return;
  10793. }
  10794. const delta = (event.wheelDelta ? event.wheelDelta : -event.deltaY);
  10795. const direction = delta > 0 ? Gallery.directions.left : Gallery.directions.right;
  10796.  
  10797. this.traverseGallery.bind(this)(direction, false);
  10798. } else if (this.thumbUnderCursor !== null && this.showOriginalContentOnHover) {
  10799. let opacity = parseFloat(Utils.getPreference(Gallery.preferences.backgroundOpacity, 1));
  10800.  
  10801. opacity -= event.deltaY * 0.0005;
  10802. opacity = Utils.clamp(opacity, "0", "1");
  10803. this.updateBackgroundOpacity(opacity);
  10804. }
  10805. }, {
  10806. passive: true
  10807. });
  10808. document.addEventListener("contextmenu", (event) => {
  10809. if (this.inGallery) {
  10810. event.preventDefault();
  10811. this.exitGallery();
  10812. }
  10813. });
  10814. document.addEventListener("keydown", (event) => {
  10815. if (!this.inGallery) {
  10816. return;
  10817. }
  10818.  
  10819. switch (event.key) {
  10820. case Gallery.directions.a:
  10821.  
  10822. case Gallery.directions.d:
  10823.  
  10824. case Gallery.directions.left:
  10825.  
  10826. case Gallery.directions.right:
  10827. this.traverseGallery(event.key, event.repeat);
  10828. break;
  10829.  
  10830. case "X":
  10831.  
  10832. case "x":
  10833. this.unFavoriteSelectedContent();
  10834. break;
  10835.  
  10836. case " ":
  10837. if (Utils.isVideo(this.getSelectedThumb())) {
  10838. const video = this.getActiveVideoPlayer();
  10839.  
  10840. if (video === document.activeElement) {
  10841. return;
  10842. }
  10843.  
  10844. if (video.paused) {
  10845. video.play().catch(() => { });
  10846. } else {
  10847. video.pause();
  10848. }
  10849. }
  10850. break;
  10851.  
  10852. case "Control":
  10853. if (!event.repeat) {
  10854. this.toggleCtrlClickOpenMediaInNewTab(true);
  10855. }
  10856. break;
  10857.  
  10858. default:
  10859. break;
  10860. }
  10861. }, {
  10862. passive: true
  10863. });
  10864. window.addEventListener("keydown", async(event) => {
  10865. if (!this.inGallery) {
  10866. return;
  10867. }
  10868. const zoomedIn = document.getElementById("main-canvas-zoom") !== null;
  10869.  
  10870. switch (event.key) {
  10871. case "F":
  10872.  
  10873. case "f":
  10874. await this.addFavoriteInGallery(event);
  10875. break;
  10876.  
  10877. case "M":
  10878.  
  10879. case "m":
  10880. if (Utils.isVideo(this.getSelectedThumb())) {
  10881. this.getActiveVideoPlayer().muted = !this.getActiveVideoPlayer().muted;
  10882. }
  10883. break;
  10884.  
  10885. case "B":
  10886.  
  10887. case "b":
  10888. this.toggleBackgroundOpacity();
  10889. break;
  10890.  
  10891. case "n":
  10892. this.toggleCursorVisibility(true);
  10893. Gallery.cursorVisibilityCooldown.restart();
  10894. break;
  10895.  
  10896. case "Escape":
  10897. this.exitGallery();
  10898. this.toggleAllVisibility(false);
  10899. break;
  10900.  
  10901. default:
  10902. break;
  10903. }
  10904. }, {
  10905. passive: true
  10906. });
  10907. window.addEventListener("keyup", (event) => {
  10908. if (!this.inGallery) {
  10909. return;
  10910. }
  10911.  
  10912. switch (event.key) {
  10913. case "Control":
  10914. this.toggleCtrlClickOpenMediaInNewTab(false);
  10915. break;
  10916.  
  10917. default:
  10918. break;
  10919. }
  10920. });
  10921. window.addEventListener("blur", () => {
  10922. this.toggleCtrlClickOpenMediaInNewTab(false);
  10923. });
  10924. }
  10925.  
  10926. /**
  10927. * @param {MouseEvent | TouchEvent} event
  10928. */
  10929. clickedOnAutoplayMenu(event) {
  10930. const autoplayMenu = document.getElementById("autoplay-menu");
  10931. return autoplayMenu !== null && autoplayMenu.contains(event.target);
  10932. }
  10933.  
  10934. addFavoritesLoaderEventListeners() {
  10935. if (Utils.onSearchPage()) {
  10936. return;
  10937. }
  10938. window.addEventListener("favoritesFetched", () => {
  10939. this.initializeThumbsForHovering.bind(this)();
  10940. this.enumerateThumbs();
  10941. });
  10942. window.addEventListener("newFavoritesFetchedOnReload", (event) => {
  10943. if (event.detail.empty) {
  10944. return;
  10945. }
  10946. this.initializeThumbsForHovering.bind(this)(event.detail.thumbs);
  10947. this.enumerateThumbs();
  10948. /**
  10949. * @type {HTMLElement[]}
  10950. */
  10951. const thumbs = event.detail.thumbs.reverse();
  10952.  
  10953. if (thumbs.length > 0) {
  10954. const thumb = thumbs[0];
  10955.  
  10956. this.upscaleAnimatedThumbsAround(thumb);
  10957. this.renderImages(thumbs
  10958. .filter(t => Utils.isImage(t))
  10959. .slice(0, 20));
  10960. }
  10961. }, {
  10962. once: true
  10963. });
  10964. window.addEventListener("startedFetchingFavorites", () => {
  10965. this.favoritesWereFetched = true;
  10966. setTimeout(() => {
  10967. const thumb = document.querySelector(`.${Utils.favoriteItemClassName}`);
  10968.  
  10969. this.renderImagesInTheBackground();
  10970.  
  10971. if (thumb !== null && !Gallery.finishedLoading) {
  10972. this.upscaleAnimatedThumbsAround(thumb);
  10973. }
  10974. }, 650);
  10975. }, {
  10976. once: true
  10977. });
  10978. window.addEventListener("favoritesLoaded", () => {
  10979. Gallery.backgroundRenderingOnPageChangeCooldown.waitTime = 1000;
  10980. Gallery.finishedLoading = true;
  10981. this.initializeThumbsForHovering.bind(this)();
  10982. this.enumerateThumbs();
  10983. this.findImageExtensionsInTheBackground();
  10984.  
  10985. if (!this.favoritesWereFetched) {
  10986. this.renderImagesInTheBackground();
  10987. }
  10988. }, {
  10989. once: true
  10990. });
  10991. window.addEventListener("newSearchResults", (event) => {
  10992. this.latestSearchResults = event.detail;
  10993. });
  10994. window.addEventListener("changedPage", () => {
  10995. this.initializeThumbsForHovering.bind(this)();
  10996. this.enumerateThumbs();
  10997.  
  10998. if (this.changedPageWhileInGallery) {
  10999. setTimeout(() => {
  11000. this.imageRenderer.postMessage({
  11001. action: "upscaleAllRenderedThumbs"
  11002. });
  11003. }, 100);
  11004. } else {
  11005. this.clearMainCanvas();
  11006. this.clearVideoSources();
  11007. this.toggleOriginalContentVisibility(false);
  11008. this.deleteAllRenders();
  11009.  
  11010. if (Gallery.settings.debugEnabled) {
  11011. Utils.getAllThumbs().forEach((thumb) => {
  11012. thumb.classList.remove("loaded");
  11013. thumb.classList.remove("debug-selected");
  11014. });
  11015. }
  11016. }
  11017. this.onPageChange();
  11018. });
  11019. window.addEventListener("foundFavorite", (event) => {
  11020. this.foundFavoriteId = event.detail;
  11021. });
  11022. window.addEventListener("shuffle", () => {
  11023. this.enumerateThumbs();
  11024. this.deleteAllRenders();
  11025. this.renderImagesInTheBackground();
  11026. });
  11027. window.addEventListener("didNotChangePageInGallery", (event) => {
  11028. if (this.inGallery) {
  11029. this.setNextSelectedThumbIndex(event.detail);
  11030. this.traverseGalleryHelper();
  11031. }
  11032. });
  11033. }
  11034.  
  11035. createImageRendererMessageHandler() {
  11036. this.imageRenderer.onmessage = (message) => {
  11037. message = message.data;
  11038.  
  11039. switch (message.action) {
  11040. case "renderCompleted":
  11041. this.onRenderCompleted(message);
  11042. break;
  11043.  
  11044. case "renderDeleted":
  11045. this.onRenderDeleted(message);
  11046. break;
  11047.  
  11048. case "extensionFound":
  11049. Utils.assignImageExtension(message.id, message.extension);
  11050. break;
  11051.  
  11052. default:
  11053. break;
  11054. }
  11055. };
  11056. }
  11057.  
  11058. addMobileEventListeners() {
  11059. if (!Utils.onMobileDevice()) {
  11060. return;
  11061. }
  11062. window.addEventListener("blur", () => {
  11063. this.deleteAllRenders();
  11064. });
  11065. document.addEventListener("touchstart", (event) => {
  11066. if (!this.inGallery) {
  11067. return;
  11068. }
  11069.  
  11070. if (!this.clickedOnAutoplayMenu(event)) {
  11071. event.preventDefault();
  11072. }
  11073. Gallery.swipeControls.set(event, true);
  11074. }, {
  11075. passive: false
  11076. });
  11077. document.addEventListener("touchend", (event) => {
  11078. if (!this.inGallery ||
  11079. // event.target.classList.contains("mobile-tap-control") ||
  11080. this.clickedOnAutoplayMenu(event)
  11081. ) {
  11082. return;
  11083. }
  11084. event.preventDefault();
  11085. Gallery.swipeControls.set(event, false);
  11086.  
  11087. if (Gallery.swipeControls.up) {
  11088. this.autoplayController.showMenu();
  11089. return;
  11090. }
  11091.  
  11092. if (Gallery.swipeControls.down) {
  11093. this.exitGallery();
  11094. this.toggleAllVisibility(false);
  11095. return;
  11096. }
  11097.  
  11098. if (Utils.isVideo(this.getSelectedThumb())) {
  11099. return;
  11100. }
  11101.  
  11102. if (Gallery.swipeControls.left) {
  11103. this.traverseGallery(Gallery.directions.right, false);
  11104. return;
  11105. }
  11106.  
  11107. if (Gallery.swipeControls.right) {
  11108. this.traverseGallery(Gallery.directions.left, false);
  11109.  
  11110. }
  11111. // this.exitGallery();
  11112. // this.toggleAllVisibility(false);
  11113.  
  11114. }, {
  11115. passive: false
  11116. });
  11117.  
  11118. window.addEventListener("orientationchange", () => {
  11119. if (this.imageRenderer !== null && this.imageRenderer !== undefined) {
  11120. this.setOrientation();
  11121. }
  11122. }, {
  11123. passive: true
  11124. });
  11125. }
  11126.  
  11127. setOrientation() {
  11128. if (!Utils.onMobileDevice()) {
  11129. return;
  11130. }
  11131. const usingLandscapeOrientation = window.screen.orientation.angle === 90;
  11132.  
  11133. this.setGifOrientation(usingLandscapeOrientation);
  11134. this.swapMainCanvasDimensions(usingLandscapeOrientation);
  11135. this.swapLowResolutionCanvasDimensions(usingLandscapeOrientation);
  11136. this.redrawCanvasesOnOrientationChange();
  11137. }
  11138.  
  11139. /**
  11140. * @param {Boolean} usingLandscapeOrientation
  11141. */
  11142. swapMainCanvasDimensions(usingLandscapeOrientation) {
  11143. this.imageRenderer.postMessage({
  11144. action: "changeCanvasOrientation",
  11145. usingLandscapeOrientation
  11146. });
  11147. }
  11148.  
  11149. /**
  11150. * @param {Boolean} usingLandscapeOrientation
  11151. */
  11152. setGifOrientation(usingLandscapeOrientation) {
  11153. const orientationId = "main-orientation";
  11154.  
  11155. if (usingLandscapeOrientation) {
  11156. Utils.insertStyleHTML(`
  11157. #original-gif-container, #main-canvas, #low-resolution-canvas {
  11158. height: 100vh !important;
  11159. width: auto !important;
  11160. }
  11161. `, orientationId);
  11162. } else {
  11163. Utils.insertStyleHTML(`
  11164. #original-gif-container, #main-canvas, #low-resolution-canvas {
  11165. width: 100vw !important;
  11166. height: auto !important;
  11167. }
  11168. `, orientationId);
  11169. }
  11170. }
  11171.  
  11172. /**
  11173. * @param {Boolean} usingLandscapeOrientation
  11174. */
  11175. swapLowResolutionCanvasDimensions(usingLandscapeOrientation) {
  11176. if (usingLandscapeOrientation === (this.lowResolutionCanvas.width > this.lowResolutionCanvas.height)) {
  11177. return;
  11178. }
  11179. const temp = this.lowResolutionCanvas.height;
  11180.  
  11181. this.lowResolutionCanvas.height = this.lowResolutionCanvas.width;
  11182. this.lowResolutionCanvas.width = temp;
  11183. }
  11184.  
  11185. redrawCanvasesOnOrientationChange() {
  11186. if (!this.inGallery) {
  11187. return;
  11188. }
  11189. const thumb = this.getSelectedThumb();
  11190.  
  11191. if (thumb === undefined || thumb === null) {
  11192. return;
  11193. }
  11194. this.drawLowResolutionCanvas(thumb);
  11195. this.imageRenderer.postMessage(this.getRenderRequest(thumb));
  11196. }
  11197.  
  11198. createMobileTapControls() {
  11199. if (!Utils.onMobileDevice()) {
  11200. return;
  11201. }
  11202. const tapControlContainer = document.createElement("div");
  11203. const leftTap = document.createElement("div");
  11204. const rightTap = document.createElement("div");
  11205.  
  11206. leftTap.className = "mobile-tap-control";
  11207. rightTap.className = "mobile-tap-control";
  11208. leftTap.id = "left-mobile-tap-control";
  11209. rightTap.id = "right-mobile-tap-control";
  11210. tapControlContainer.appendChild(leftTap);
  11211. tapControlContainer.appendChild(rightTap);
  11212. this.originalContentContainer.appendChild(tapControlContainer);
  11213. Utils.insertStyleHTML(`
  11214. .mobile-tap-control {
  11215. position: fixed;
  11216. top: 50%;
  11217. height: 65vh;
  11218. width: 25vw;
  11219. opacity: 0;
  11220. background: red;
  11221. z-index: 9999;
  11222. color: red;
  11223. transform: translateY(-50%);
  11224. }
  11225.  
  11226. #left-mobile-tap-control {
  11227. left: 0;
  11228. }
  11229.  
  11230. #right-mobile-tap-control {
  11231. right: 0;
  11232. }
  11233. `);
  11234. this.toggleTapTraversal(false);
  11235. leftTap.ontouchend = () => {
  11236. if (this.inGallery) {
  11237. this.traverseGallery(Gallery.directions.left, false);
  11238. }
  11239. };
  11240. rightTap.ontouchend = () => {
  11241. if (this.inGallery) {
  11242. this.traverseGallery(Gallery.directions.right, false);
  11243. }
  11244. };
  11245. }
  11246.  
  11247. /**
  11248. * @param {Boolean} value
  11249. */
  11250. toggleTapTraversal(value) {
  11251. Utils.insertStyleHTML(`
  11252. .mobile-tap-control {
  11253. pointer-events: ${value ? "auto" : "none"};
  11254. }
  11255. `, "tap-traversal");
  11256. }
  11257.  
  11258. addMemoryManagementEventListeners() {
  11259. if (Utils.onFavoritesPage()) {
  11260. return;
  11261. }
  11262. window.addEventListener("blur", () => {
  11263. this.leftPage = true;
  11264. this.deleteAllRenders();
  11265. this.clearInactiveVideoSources();
  11266. });
  11267. window.addEventListener("focus", () => {
  11268. if (this.leftPage) {
  11269. this.renderImagesInTheBackground();
  11270. this.leftPage = false;
  11271. }
  11272. });
  11273. }
  11274.  
  11275. async prepareSearchPage() {
  11276. if (!Utils.onSearchPage()) {
  11277. return;
  11278. }
  11279. await Utils.findImageExtensionsOnSearchPage();
  11280. dispatchEvent(new Event("foundExtensionsOnSearchPage"));
  11281. this.renderImagesInTheBackground();
  11282. }
  11283.  
  11284. insertHTML() {
  11285. this.insertStyleHTML();
  11286. this.insertDebugHTML();
  11287. this.insertOptionsHTML();
  11288. this.insertOriginalContentContainerHTML();
  11289.  
  11290. }
  11291.  
  11292. insertStyleHTML() {
  11293. Utils.insertStyleHTML(Gallery.galleryHTML, "gallery");
  11294. }
  11295.  
  11296. insertDebugHTML() {
  11297. if (Gallery.settings.debugEnabled) {
  11298. Utils.insertStyleHTML(Gallery.galleryDebugHTML, "gallery-debug");
  11299. }
  11300. }
  11301.  
  11302. insertOptionsHTML() {
  11303. this.insertShowOnHoverOption();
  11304. }
  11305.  
  11306. insertShowOnHoverOption() {
  11307. let optionId = "show-content-on-hover";
  11308. let optionText = "Fullscreen on Hover";
  11309. let optionTitle = "View full resolution images or play videos and GIFs when hovering over a thumbnail";
  11310. let optionIsChecked = this.showOriginalContentOnHover;
  11311. let onOptionChanged = (event) => {
  11312. Utils.setPreference(Gallery.preferences.showOnHover, event.target.checked);
  11313. this.toggleAllVisibility(event.target.checked);
  11314. };
  11315.  
  11316. if (Utils.onMobileDevice()) {
  11317. optionId = "mobile-gallery-checkbox";
  11318. optionText = "Gallery";
  11319. optionTitle = "View full resolution images/play videos when a thumbnail is clicked";
  11320. optionIsChecked = this.enlargeOnClickOnMobile;
  11321. onOptionChanged = (event) => {
  11322. Utils.setPreference(Gallery.preferences.enlargeOnClick, event.target.checked);
  11323. this.enlargeOnClickOnMobile = event.target.checked;
  11324. };
  11325. }
  11326. Utils.createFavoritesOption(
  11327. optionId,
  11328. optionText,
  11329. optionTitle,
  11330. optionIsChecked,
  11331. onOptionChanged,
  11332. true
  11333. // "(Middle Click)"
  11334. );
  11335. }
  11336.  
  11337. insertOriginalContentContainerHTML() {
  11338. const originalContentContainerHTML = `
  11339. <div id="gallery-container">
  11340. <a id="original-video-container">
  11341. <video ${Gallery.commonVideoAttributes} active></video>
  11342. </a>
  11343. <img id="original-gif-container"></img>
  11344. <a id="original-content-background-link-mask"></a>
  11345. <a id="original-content-background"></a>
  11346. </div>
  11347. `;
  11348.  
  11349. Utils.insertFavoritesSearchGalleryHTML("afterbegin", originalContentContainerHTML);
  11350. this.originalContentContainer = document.getElementById("gallery-container");
  11351. this.originalContentContainer.insertBefore(this.lowResolutionCanvas, this.originalContentContainer.firstChild);
  11352. this.originalContentContainer.insertBefore(this.mainCanvas, this.originalContentContainer.firstChild);
  11353. this.background = document.getElementById("original-content-background");
  11354.  
  11355. this.originalImageLinkMask = document.getElementById("original-content-background-link-mask");
  11356. this.videoContainer = document.getElementById("original-video-container");
  11357. this.addAdditionalVideoPlayers();
  11358. this.videoPlayers = Array.from(this.videoContainer.querySelectorAll("video"));
  11359. this.addVideoPlayerEventListeners();
  11360. this.loadVideoVolume();
  11361. this.gifContainer = document.getElementById("original-gif-container");
  11362. this.mainCanvas.id = "main-canvas";
  11363. this.lowResolutionCanvas.id = "low-resolution-canvas";
  11364. this.lowResolutionCanvas.width = Utils.onMobileDevice() ? 320 : 1280;
  11365. this.lowResolutionCanvas.height = Utils.onMobileDevice() ? 180 : 720;
  11366. this.toggleOriginalContentVisibility(false);
  11367. this.addBackgroundEventListeners();
  11368.  
  11369. if (Autoplay.disabled || !this.autoplayController.active || this.autoplayController.paused) {
  11370. this.toggleVideoLooping(true);
  11371. } else {
  11372. this.toggleVideoLooping(false);
  11373. }
  11374. }
  11375.  
  11376. addAdditionalVideoPlayers() {
  11377. const videoPlayerHTML = `<video ${Gallery.commonVideoAttributes}></video>`;
  11378.  
  11379. for (let i = 0; i < Gallery.settings.additionalVideoPlayerCount; i += 1) {
  11380. this.videoContainer.insertAdjacentHTML("beforeend", videoPlayerHTML);
  11381. }
  11382. }
  11383.  
  11384. addVideoPlayerEventListeners() {
  11385. this.videoContainer.onclick = (event) => {
  11386. if (!event.ctrlKey) {
  11387. event.preventDefault();
  11388. }
  11389. };
  11390.  
  11391. for (const video of this.videoPlayers) {
  11392. video.addEventListener("mousemove", () => {
  11393. if (!video.hasAttribute("controls")) {
  11394. video.setAttribute("controls", "");
  11395. }
  11396. }, {
  11397. passive: true
  11398. });
  11399. video.addEventListener("click", (event) => {
  11400. if (event.ctrlKey) {
  11401. return;
  11402. }
  11403.  
  11404. if (video.paused) {
  11405. video.play().catch(() => { });
  11406. } else {
  11407. video.pause();
  11408. }
  11409. }, {
  11410. passive: true
  11411. });
  11412. video.addEventListener("volumechange", (event) => {
  11413. if (!event.target.hasAttribute("active")) {
  11414. return;
  11415. }
  11416. Utils.setPreference(Gallery.preferences.videoVolume, video.volume);
  11417. Utils.setPreference(Gallery.preferences.videoMuted, video.muted);
  11418.  
  11419. for (const v of this.getInactiveVideoPlayers()) {
  11420. v.volume = video.volume;
  11421. v.muted = video.muted;
  11422. }
  11423. }, {
  11424. passive: true
  11425. });
  11426. video.addEventListener("ended", () => {
  11427. this.autoplayController.onVideoEnded();
  11428. }, {
  11429. passive: true
  11430. });
  11431. video.addEventListener("dblclick", () => {
  11432. if (this.inGallery && !this.recentlyEnteredGallery) {
  11433. this.exitGallery();
  11434. this.toggleAllVisibility(false);
  11435. }
  11436. });
  11437.  
  11438. if (Utils.onMobileDevice()) {
  11439. video.addEventListener("touchend", () => {
  11440. this.toggleVideoControls(true);
  11441. }, {
  11442. passive: true
  11443. });
  11444. }
  11445. }
  11446. }
  11447.  
  11448. addBackgroundEventListeners() {
  11449. if (Utils.onMobileDevice()) {
  11450. return;
  11451. }
  11452. this.background.addEventListener("mousemove", () => {
  11453. Gallery.cursorVisibilityCooldown.restart();
  11454. this.toggleCursorVisibility(true);
  11455. }, {
  11456. passive: true
  11457. });
  11458. Gallery.cursorVisibilityCooldown.onCooldownEnd = () => {
  11459. if (this.inGallery) {
  11460. this.toggleCursorVisibility(false);
  11461. }
  11462. };
  11463. }
  11464.  
  11465. loadVideoVolume() {
  11466. const video = this.getActiveVideoPlayer();
  11467.  
  11468. video.volume = parseFloat(Utils.getPreference(Gallery.preferences.videoVolume, 1));
  11469. video.muted = Utils.getPreference(Gallery.preferences.videoMuted, true);
  11470. }
  11471.  
  11472. /**
  11473. * @param {Number} opacity
  11474. */
  11475. updateBackgroundOpacity(opacity) {
  11476. this.background.style.opacity = opacity;
  11477. Utils.setPreference(Gallery.preferences.backgroundOpacity, opacity);
  11478. }
  11479.  
  11480. createAutoplayController() {
  11481. const subscribers = new AutoplayListenerList(
  11482. () => {
  11483. this.toggleVideoLooping(false);
  11484. },
  11485. () => {
  11486. this.toggleVideoLooping(true);
  11487. },
  11488. () => {
  11489. this.toggleVideoLooping(true);
  11490. },
  11491. () => {
  11492. this.toggleVideoLooping(false);
  11493. },
  11494. () => {
  11495. if (this.inGallery) {
  11496. const direction = Autoplay.settings.moveForward ? Gallery.directions.right : Gallery.directions.left;
  11497.  
  11498. this.traverseGallery(direction, false);
  11499. }
  11500. },
  11501. () => {
  11502. if (this.inGallery && Utils.isVideo(this.getSelectedThumb())) {
  11503. this.playOriginalVideo(this.getSelectedThumb());
  11504. }
  11505. }
  11506. );
  11507.  
  11508. this.autoplayController = new Autoplay(subscribers);
  11509. }
  11510.  
  11511. /**
  11512. * @param {HTMLElement[]} thumbs
  11513. */
  11514. initializeThumbsForHovering(thumbs) {
  11515. const thumbElements = thumbs === undefined ? Utils.getAllThumbs() : thumbs;
  11516.  
  11517. for (const thumbElement of thumbElements) {
  11518. this.addEventListenersToThumb(thumbElement);
  11519. }
  11520. }
  11521.  
  11522. renderImagesInTheBackground() {
  11523. if (Utils.onMobileDevice()) {
  11524. return;
  11525. }
  11526. const thumbs = Utils.getAllThumbs();
  11527.  
  11528. if (Utils.onSearchPage()) {
  11529. this.renderImages(thumbs.filter(thumb => Utils.isImage(thumb)).slice(0, 50));
  11530. return;
  11531. }
  11532. const animatedThumbs = thumbs
  11533. .slice(0, Gallery.settings.animatedThumbsToUpscaleDiscrete)
  11534. .filter(thumb => !Utils.isImage(thumb));
  11535.  
  11536. if (thumbs.length > 0) {
  11537. this.upscaleAnimatedThumbs(animatedThumbs);
  11538. this.renderImagesAround(thumbs[0]);
  11539. }
  11540. }
  11541.  
  11542. onPageChange() {
  11543. this.onPageChangeHelper();
  11544. this.foundFavoriteId = null;
  11545. this.changedPageInGalleryDirection = null;
  11546. }
  11547.  
  11548. onPageChangeHelper() {
  11549. if (this.visibleThumbs.length <= 0) {
  11550. return;
  11551. }
  11552.  
  11553. if (this.changedPageInGalleryDirection !== null) {
  11554. this.onPageChangedInGallery();
  11555. return;
  11556. }
  11557.  
  11558. if (this.foundFavoriteId !== null) {
  11559. this.onFavoriteFound();
  11560. return;
  11561. }
  11562. setTimeout(() => {
  11563. if (Gallery.backgroundRenderingOnPageChangeCooldown.ready) {
  11564. this.renderImagesInTheBackground();
  11565. }
  11566. }, 100);
  11567. }
  11568.  
  11569. onPageChangedInGallery() {
  11570. if (this.changedPageInGalleryDirection === "ArrowRight") {
  11571. this.currentlySelectedThumbIndex = 0;
  11572. } else {
  11573. this.currentlySelectedThumbIndex = this.visibleThumbs.length - 1;
  11574. }
  11575. this.traverseGalleryHelper();
  11576. }
  11577.  
  11578. onFavoriteFound() {
  11579. const thumb = document.getElementById(this.foundFavoriteId);
  11580.  
  11581. if (thumb !== null) {
  11582. this.renderImagesAround(thumb);
  11583. }
  11584. }
  11585.  
  11586. /**
  11587. * @param {HTMLElement[]} imagesToRender
  11588. */
  11589. renderImages(imagesToRender) {
  11590. const renderRequests = imagesToRender.map(image => this.getRenderRequest(image));
  11591. const canvases = Utils.onSearchPage() ? [] : renderRequests
  11592. .filter(request => request.canvas !== undefined)
  11593. .map(request => request.canvas);
  11594.  
  11595. this.imageRenderer.postMessage({
  11596. action: "renderMultiple",
  11597. id: this.currentBatchRenderRequestId,
  11598. renderRequests,
  11599. requestType: "none"
  11600. }, canvases);
  11601. this.currentBatchRenderRequestId += 1;
  11602.  
  11603. if (this.currentBatchRenderRequestId >= 1000) {
  11604. this.currentBatchRenderRequestId = 0;
  11605. }
  11606. }
  11607.  
  11608. /**
  11609. * @param {Object} message
  11610. */
  11611. onRenderCompleted(message) {
  11612. const thumb = document.getElementById(message.id);
  11613.  
  11614. this.completedRenders.add(message.id);
  11615.  
  11616. if (Gallery.settings.debugEnabled) {
  11617.  
  11618. if (Gallery.settings.loopAtEndOfGallery) {
  11619. if (thumb !== null) {
  11620. thumb.classList.add("loaded");
  11621. }
  11622. } else {
  11623. const post = Post.allPosts.get(message.id);
  11624.  
  11625. if (post !== undefined && post.root !== undefined) {
  11626. post.root.classList.add("loaded");
  11627. }
  11628. }
  11629. }
  11630.  
  11631. if (thumb !== null && message.extension === "gif") {
  11632. Utils.getImageFromThumb(thumb).setAttribute("gif", true);
  11633. return;
  11634. }
  11635. Utils.assignImageExtension(message.id, message.extension);
  11636. this.drawMainCanvasOnRenderCompleted(thumb);
  11637. }
  11638.  
  11639. /**
  11640. * @param {HTMLElement} thumb
  11641. */
  11642. drawMainCanvasOnRenderCompleted(thumb) {
  11643. if (thumb === null) {
  11644. return;
  11645. }
  11646. const mainCanvasIsVisible = this.showOriginalContentOnHover || this.inGallery;
  11647.  
  11648. if (!mainCanvasIsVisible) {
  11649. return;
  11650. }
  11651. const selectedThumb = this.getSelectedThumb();
  11652. const selectedThumbIsImage = selectedThumb !== undefined && Utils.isImage(selectedThumb);
  11653.  
  11654. if (!selectedThumbIsImage) {
  11655. return;
  11656. }
  11657.  
  11658. if (selectedThumb.id === thumb.id) {
  11659. this.drawMainCanvas(thumb);
  11660. }
  11661. }
  11662.  
  11663. onRenderDeleted(message) {
  11664. const thumb = document.getElementById(message.id);
  11665.  
  11666. if (thumb !== null) {
  11667. if (Gallery.settings.debugEnabled) {
  11668. thumb.classList.remove("loaded");
  11669. }
  11670. }
  11671. this.startedRenders.delete(message.id);
  11672. this.completedRenders.delete(message.id);
  11673. }
  11674.  
  11675. deleteAllRenders() {
  11676. this.startedRenders.clear();
  11677. this.completedRenders.clear();
  11678. this.deleteAllTransferredCanvases();
  11679. this.imageRenderer.postMessage({
  11680. action: "deleteAllRenders"
  11681. });
  11682.  
  11683. if (Gallery.settings.debugEnabled) {
  11684. if (Gallery.settings.loopAtEndOfGallery) {
  11685. for (const thumb of this.visibleThumbs) {
  11686. thumb.classList.remove("loaded");
  11687. }
  11688. } else {
  11689. for (const post of Post.allPosts.values()) {
  11690. if (post.root !== undefined) {
  11691. post.root.classList.remove("loaded");
  11692. }
  11693. }
  11694. }
  11695. }
  11696. }
  11697.  
  11698. deleteAllTransferredCanvases() {
  11699. if (Utils.onSearchPage()) {
  11700. return;
  11701. }
  11702.  
  11703. for (const id of this.transferredCanvases.keys()) {
  11704. this.transferredCanvases.get(id).remove();
  11705. this.transferredCanvases.delete(id);
  11706. }
  11707. this.transferredCanvases.clear();
  11708. }
  11709.  
  11710. /**
  11711. * @param {HTMLElement} thumb
  11712. * @returns {HTMLCanvasElement}
  11713. */
  11714. getCanvasFromThumb(thumb) {
  11715. let canvas = thumb.querySelector("canvas");
  11716.  
  11717. if (canvas === null) {
  11718. canvas = document.createElement("canvas");
  11719. thumb.children[0].appendChild(canvas);
  11720. }
  11721. return canvas;
  11722. }
  11723.  
  11724. /**
  11725. * @param {HTMLElement} thumb
  11726. * @returns {HTMLCanvasElement}
  11727. */
  11728. getOffscreenCanvasFromThumb(thumb) {
  11729. const canvas = this.getCanvasFromThumb(thumb);
  11730.  
  11731. this.transferredCanvases.set(thumb.id, canvas);
  11732. return canvas.transferControlToOffscreen();
  11733. }
  11734.  
  11735. hideCaptionsWhenShowingOriginalContent() {
  11736. for (const caption of document.getElementsByClassName("caption")) {
  11737. if (this.showOriginalContentOnHover) {
  11738. caption.classList.add("hide");
  11739. } else {
  11740. caption.classList.remove("hide");
  11741. }
  11742. }
  11743. }
  11744.  
  11745. async findImageExtensionsInTheBackground() {
  11746. await Utils.sleep(1000);
  11747. const idsWithUnknownExtensions = this.getIdsWithUnknownExtensions(Array.from(Post.allPosts.values()));
  11748.  
  11749. while (idsWithUnknownExtensions.length > 0) {
  11750. await Utils.sleep(3000);
  11751.  
  11752. while (idsWithUnknownExtensions.length > 0 && Gallery.finishedLoading) {
  11753. const id = idsWithUnknownExtensions.pop();
  11754.  
  11755. if (id !== undefined && id !== null && !Utils.extensionIsKnown(id)) {
  11756. this.imageRenderer.postMessage({
  11757. action: "findExtension",
  11758. id
  11759. });
  11760. await Utils.sleep(10);
  11761. }
  11762. }
  11763. }
  11764. Gallery.settings.extensionsFoundBeforeSavingCount = 0;
  11765. }
  11766.  
  11767. enumerateThumbs() {
  11768. this.visibleThumbs = Utils.getAllThumbs();
  11769. this.enumeratedThumbs.clear();
  11770.  
  11771. for (let i = 0; i < this.visibleThumbs.length; i += 1) {
  11772. this.enumerateThumb(this.visibleThumbs[i], i);
  11773. }
  11774. }
  11775.  
  11776. /**
  11777. * @param {HTMLElement} thumb
  11778. * @param {Number} index
  11779. */
  11780. enumerateThumb(thumb, index) {
  11781. this.enumeratedThumbs.set(thumb.id, index);
  11782. }
  11783.  
  11784. /**
  11785. * @param {HTMLElement} thumb
  11786. * @returns {Number | null}
  11787. */
  11788. getIndexFromThumb(thumb) {
  11789. return this.enumeratedThumbs.get(thumb.id) || 0;
  11790. }
  11791.  
  11792. /**
  11793. * @param {HTMLElement} thumb
  11794. */
  11795. addEventListenersToThumb(thumb) {
  11796. if (Utils.onMobileDevice()) {
  11797. return;
  11798. }
  11799. const image = Utils.getImageFromThumb(thumb);
  11800.  
  11801. if (image.onmouseover !== null) {
  11802. return;
  11803. }
  11804. image.onmouseover = (event) => {
  11805. if (this.inGallery || this.recentlyExitedGallery || Utils.enteredOverCaptionTag(event)) {
  11806. return;
  11807. }
  11808. this.thumbUnderCursor = thumb;
  11809. this.lastEnteredThumb = thumb;
  11810. this.showOriginalContent(thumb);
  11811. };
  11812. image.onmouseout = (event) => {
  11813. this.thumbUnderCursor = null;
  11814.  
  11815. if (this.inGallery || Utils.enteredOverCaptionTag(event)) {
  11816. return;
  11817. }
  11818. this.stopAllVideos();
  11819. this.hideOriginalContent();
  11820. };
  11821. }
  11822.  
  11823. /**
  11824. * @param {HTMLElement} thumb
  11825. */
  11826. openPostInNewPage(thumb) {
  11827. thumb = thumb === undefined || thumb === null ? this.getSelectedThumb() : thumb;
  11828. Utils.openPostInNewTab(Utils.getIdFromThumb(thumb));
  11829. }
  11830.  
  11831. unFavoriteSelectedContent() {
  11832. if (!Utils.userIsOnTheirOwnFavoritesPage()) {
  11833. return;
  11834. }
  11835. const selectedThumb = this.getSelectedThumb();
  11836.  
  11837. if (selectedThumb === null) {
  11838. return;
  11839. }
  11840. const removeFavoriteButton = Utils.getRemoveFavoriteButtonFromThumb(selectedThumb);
  11841.  
  11842. if (removeFavoriteButton === null) {
  11843. return;
  11844. }
  11845. const showRemoveFavoriteButtons = document.getElementById("show-remove-favorite-buttons");
  11846.  
  11847. if (showRemoveFavoriteButtons === null) {
  11848. return;
  11849. }
  11850.  
  11851. if (!Gallery.addOrRemoveFavoriteCooldown.ready) {
  11852. return;
  11853. }
  11854.  
  11855. if (!showRemoveFavoriteButtons.checked) {
  11856. Utils.showFullscreenIcon(Utils.icons.warning, 1000);
  11857. setTimeout(() => {
  11858. alert("The \"Remove Buttons\" option must be checked to use this hotkey");
  11859. }, 20);
  11860. return;
  11861. }
  11862. Utils.showFullscreenIcon(Utils.icons.heartMinus);
  11863. this.onFavoriteAddedOrDeleted(selectedThumb.id);
  11864. Utils.removeFavorite(selectedThumb.id);
  11865. }
  11866.  
  11867. enterGallery() {
  11868. const selectedThumb = this.getSelectedThumb();
  11869.  
  11870. this.toggleTapTraversal(true);
  11871. this.lastSelectedThumbIndexBeforeEnteringGallery = this.currentlySelectedThumbIndex;
  11872. this.background.style.pointerEvents = "auto";
  11873.  
  11874. if (Utils.isVideo(selectedThumb)) {
  11875. this.toggleVideoControls(true);
  11876. }
  11877. this.inGallery = true;
  11878. dispatchEvent(new CustomEvent("showOriginalContent", {
  11879. detail: true
  11880. }));
  11881. this.autoplayController.start(selectedThumb);
  11882. Gallery.cursorVisibilityCooldown.restart();
  11883. this.recentlyEnteredGallery = true;
  11884. setTimeout(() => {
  11885. this.recentlyEnteredGallery = false;
  11886. }, 300);
  11887. this.setupOriginalImageLinkInGallery();
  11888. }
  11889.  
  11890. exitGallery() {
  11891. if (Gallery.settings.debugEnabled) {
  11892. Utils.getAllThumbs().forEach(thumb => thumb.classList.remove("debug-selected"));
  11893. }
  11894. this.toggleTapTraversal(false);
  11895. this.toggleCursorVisibility(true);
  11896. this.toggleVideoControls(false);
  11897. this.background.style.pointerEvents = "none";
  11898. this.toggleCtrlClickOpenMediaInNewTab(false);
  11899. const thumbIndex = this.getIndexOfThumbUnderCursor();
  11900.  
  11901. if (Utils.onMobileDevice()) {
  11902. this.hideOriginalContent();
  11903. this.deleteAllRenders();
  11904. }
  11905.  
  11906. if (!Utils.onMobileDevice() && thumbIndex !== this.lastSelectedThumbIndexBeforeEnteringGallery) {
  11907. this.hideOriginalContent();
  11908.  
  11909. if (thumbIndex !== null && this.showOriginalContentOnHover) {
  11910. this.showOriginalContent(this.visibleThumbs[thumbIndex]);
  11911. }
  11912. }
  11913.  
  11914. this.recentlyExitedGallery = true;
  11915. setTimeout(() => {
  11916. this.recentlyExitedGallery = false;
  11917. }, 300);
  11918. this.inGallery = false;
  11919. this.autoplayController.stop();
  11920. document.dispatchEvent(new Event("mousemove"));
  11921. }
  11922.  
  11923. /**
  11924. * @param {String} direction
  11925. * @param {Boolean} keyIsHeldDown
  11926. */
  11927. traverseGallery(direction, keyIsHeldDown) {
  11928. if (Gallery.settings.debugEnabled) {
  11929. this.getSelectedThumb().classList.remove("debug-selected");
  11930. }
  11931.  
  11932. if (keyIsHeldDown && !Gallery.keyHeldDownTraversalCooldown.ready) {
  11933. return;
  11934. }
  11935.  
  11936. if (!Gallery.settings.loopAtEndOfGallery && this.reachedEndOfGallery(direction) && Gallery.finishedLoading) {
  11937. this.changedPageInGalleryDirection = direction;
  11938. dispatchEvent(new CustomEvent("reachedEndOfGallery", {
  11939. detail: direction
  11940. }));
  11941. return;
  11942. }
  11943. this.setNextSelectedThumbIndex(direction);
  11944. this.traverseGalleryHelper();
  11945. }
  11946.  
  11947. traverseGalleryHelper() {
  11948. const selectedThumb = this.getSelectedThumb();
  11949.  
  11950. this.autoplayController.startViewTimer(selectedThumb);
  11951. this.clearOriginalContentSources();
  11952. this.stopAllVideos();
  11953.  
  11954. if (Gallery.settings.debugEnabled) {
  11955. selectedThumb.classList.add("debug-selected");
  11956. }
  11957. this.upscaleAnimatedThumbsAround(selectedThumb);
  11958. this.renderImagesAround(selectedThumb);
  11959. this.preloadInactiveVideoPlayers(selectedThumb);
  11960.  
  11961. if (!Utils.usingFirefox()) {
  11962. Utils.scrollToThumb(selectedThumb.id, false, true);
  11963. }
  11964.  
  11965. if (Utils.isVideo(selectedThumb)) {
  11966. this.toggleVideoControls(true);
  11967. this.showOriginalVideo(selectedThumb);
  11968. } else if (Utils.isGif(selectedThumb)) {
  11969. this.toggleVideoControls(false);
  11970. this.toggleOriginalVideoContainer(false);
  11971. this.showOriginalGIF(selectedThumb);
  11972. } else {
  11973. this.toggleVideoControls(false);
  11974. this.toggleOriginalVideoContainer(false);
  11975. this.showOriginalImage(selectedThumb);
  11976. }
  11977. this.setupOriginalImageLinkInGallery();
  11978.  
  11979. if (Utils.onMobileDevice()) {
  11980. this.toggleVideoControls(false);
  11981. }
  11982. }
  11983.  
  11984. /**
  11985. * @param {String} direction
  11986. * @returns {Boolean}
  11987. */
  11988. reachedEndOfGallery(direction) {
  11989. if (direction === Gallery.directions.right && this.currentlySelectedThumbIndex >= this.visibleThumbs.length - 1) {
  11990. return true;
  11991. }
  11992.  
  11993. if (direction === Gallery.directions.left && this.currentlySelectedThumbIndex <= 0) {
  11994. return true;
  11995. }
  11996. return false;
  11997. }
  11998.  
  11999. /**
  12000. * @param {String} direction
  12001. * @returns {Boolean}
  12002. */
  12003. setNextSelectedThumbIndex(direction) {
  12004. if (direction === Gallery.directions.left || direction === Gallery.directions.a) {
  12005. this.currentlySelectedThumbIndex -= 1;
  12006. this.currentlySelectedThumbIndex = this.currentlySelectedThumbIndex < 0 ? this.visibleThumbs.length - 1 : this.currentlySelectedThumbIndex;
  12007. } else {
  12008. this.currentlySelectedThumbIndex += 1;
  12009. this.currentlySelectedThumbIndex = this.currentlySelectedThumbIndex >= this.visibleThumbs.length ? 0 : this.currentlySelectedThumbIndex;
  12010. }
  12011. return false;
  12012. }
  12013.  
  12014. /**
  12015. * @param {Boolean} value
  12016. */
  12017. toggleAllVisibility(value) {
  12018. this.showOriginalContentOnHover = value === undefined ? !this.showOriginalContentOnHover : value;
  12019. this.toggleOriginalContentVisibility(this.showOriginalContentOnHover);
  12020.  
  12021. if (this.thumbUnderCursor !== null) {
  12022. this.toggleBackgroundVisibility();
  12023. this.toggleScrollbarVisibility();
  12024. }
  12025. dispatchEvent(new CustomEvent("showOriginalContent", {
  12026. detail: this.showOriginalContentOnHover
  12027. }));
  12028. Utils.setPreference(Gallery.preferences.showOnHover, this.showOriginalContentOnHover);
  12029.  
  12030. const showOnHoverCheckbox = document.getElementById("show-content-on-hover-checkbox");
  12031.  
  12032. if (showOnHoverCheckbox !== null) {
  12033. showOnHoverCheckbox.checked = this.showOriginalContentOnHover;
  12034. }
  12035. }
  12036.  
  12037. hideOriginalContent() {
  12038. this.toggleBackgroundVisibility(false);
  12039. this.toggleScrollbarVisibility(true);
  12040. this.clearOriginalContentSources();
  12041. this.stopAllVideos();
  12042. this.clearMainCanvas();
  12043. this.toggleOriginalVideoContainer(false);
  12044. this.toggleOriginalGIF(false);
  12045. }
  12046.  
  12047. clearOriginalContentSources() {
  12048. this.mainCanvas.style.visibility = "hidden";
  12049. this.lowResolutionCanvas.style.visibility = "hidden";
  12050. this.gifContainer.src = "";
  12051. this.gifContainer.style.visibility = "hidden";
  12052. }
  12053.  
  12054. /**
  12055. * @returns {Boolean}
  12056. */
  12057. currentlyHoveringOverVideoThumb() {
  12058. if (this.thumbUnderCursor === null) {
  12059. return false;
  12060. }
  12061. return Utils.isVideo(this.thumbUnderCursor);
  12062. }
  12063.  
  12064. /**
  12065. * @param {HTMLElement} thumb
  12066. */
  12067. showOriginalContent(thumb) {
  12068. this.currentlySelectedThumbIndex = this.getIndexFromThumb(thumb);
  12069. this.upscaleAnimatedThumbsAroundDiscrete(thumb);
  12070.  
  12071. if (!this.inGallery && Gallery.settings.renderAroundAggressively) {
  12072. this.renderImagesAround(thumb);
  12073. }
  12074.  
  12075. if (Utils.isVideo(thumb)) {
  12076. this.showOriginalVideo(thumb);
  12077. } else if (Utils.isGif(thumb)) {
  12078. this.showOriginalGIF(thumb);
  12079. } else {
  12080. this.showOriginalImage(thumb);
  12081. }
  12082.  
  12083. if (this.showOriginalContentOnHover) {
  12084. this.toggleBackgroundVisibility(true);
  12085. this.toggleScrollbarVisibility(false);
  12086. }
  12087. }
  12088.  
  12089. /**
  12090. * @param {HTMLElement} thumb
  12091. */
  12092. showOriginalVideo(thumb) {
  12093. if (!this.showOriginalContentOnHover) {
  12094. return;
  12095. }
  12096. this.toggleMainCanvas(false);
  12097. this.videoContainer.style.display = "block";
  12098. this.playOriginalVideo(thumb);
  12099.  
  12100. if (!this.inGallery) {
  12101. this.toggleVideoControls(false);
  12102. }
  12103. }
  12104.  
  12105. /**
  12106. * @param {HTMLElement} initialThumb
  12107. */
  12108. preloadInactiveVideoPlayers(initialThumb) {
  12109. if (!this.inGallery || Gallery.settings.additionalVideoPlayerCount < 1) {
  12110. return;
  12111. }
  12112. this.setActiveVideoPlayer(initialThumb);
  12113. const inactiveVideoPlayers = this.getInactiveVideoPlayers();
  12114. const videoThumbsAroundInitialThumb = this.getAdjacentVideoThumbs(initialThumb, inactiveVideoPlayers.length);
  12115. const loadedVideoSources = new Set(inactiveVideoPlayers
  12116. .map(video => video.src)
  12117. .filter(src => src !== ""));
  12118. const videoSourcesAroundInitialThumb = new Set(videoThumbsAroundInitialThumb.map(thumb => this.getVideoSource(thumb)));
  12119. const videoThumbsNotLoaded = videoThumbsAroundInitialThumb.filter(thumb => !loadedVideoSources.has(this.getVideoSource(thumb)));
  12120. const freeInactiveVideoPlayers = inactiveVideoPlayers.filter(video => !videoSourcesAroundInitialThumb.has(video.src));
  12121.  
  12122. for (let i = 0; i < freeInactiveVideoPlayers.length && i < videoThumbsNotLoaded.length; i += 1) {
  12123. this.setVideoSource(freeInactiveVideoPlayers[i], videoThumbsNotLoaded[i]);
  12124. }
  12125. this.stopAllVideos();
  12126. }
  12127.  
  12128. /**
  12129. * @param {HTMLElement} initialThumb
  12130. * @param {Number} limit
  12131. * @returns {HTMLElement[]}
  12132. */
  12133. getAdjacentVideoThumbs(initialThumb, limit) {
  12134. if (Gallery.settings.loopAtEndOfGallery) {
  12135. return this.getAdjacentVideoThumbsOnCurrentPage(initialThumb, limit);
  12136. }
  12137. return this.getAdjacentVideoThumbsThroughoutAllPages(initialThumb, limit);
  12138. }
  12139.  
  12140. /**
  12141. * @param {HTMLElement} initialThumb
  12142. * @param {Number} limit
  12143. * @returns {HTMLElement[]}
  12144. */
  12145. getAdjacentVideoThumbsOnCurrentPage(initialThumb, limit) {
  12146. return this.getAdjacentThumbsLooped(
  12147. initialThumb,
  12148. limit,
  12149. (t) => {
  12150. return Utils.isVideo(t) && t.id !== initialThumb.id;
  12151. }
  12152. );
  12153.  
  12154. }
  12155.  
  12156. /**
  12157. * @param {HTMLElement} initialThumb
  12158. * @param {Number} limit
  12159. * @returns {HTMLElement[]}
  12160. */
  12161. getAdjacentVideoThumbsThroughoutAllPages(initialThumb, limit) {
  12162. return this.getAdjacentSearchResults(
  12163. initialThumb,
  12164. limit,
  12165. (t) => {
  12166. return Utils.isVideo(t) && t.id !== initialThumb.id;
  12167. }
  12168. );
  12169. }
  12170.  
  12171. /**
  12172. * @param {HTMLElement} thumb
  12173. * @returns {String}
  12174. */
  12175. getVideoSource(thumb) {
  12176. return Utils.getOriginalImageURLFromThumb(thumb).replace("jpg", "mp4");
  12177. }
  12178.  
  12179. /**
  12180. * @param {HTMLVideoElement} video
  12181. * @param {HTMLElement} thumb
  12182. */
  12183. setVideoSource(video, thumb) {
  12184. if (this.videoPlayerHasSource(video, thumb)) {
  12185. return;
  12186. }
  12187. this.createVideoClip(video, thumb);
  12188. video.src = this.getVideoSource(thumb);
  12189. }
  12190.  
  12191. /**
  12192. * @param {HTMLVideoElement} video
  12193. * @param {HTMLElement} thumb
  12194. */
  12195. createVideoClip(video, thumb) {
  12196. const videoClip = this.videoClips.get(thumb.id);
  12197.  
  12198. if (videoClip === undefined) {
  12199. video.ontimeupdate = null;
  12200. return;
  12201. }
  12202. video.ontimeupdate = () => {
  12203. if (video.currentTime < videoClip.start || video.currentTime > videoClip.end) {
  12204. video.removeAttribute("controls");
  12205. video.currentTime = videoClip.start;
  12206. }
  12207. };
  12208. }
  12209.  
  12210. clearVideoSources() {
  12211. for (const video of this.videoPlayers) {
  12212. video.src = "";
  12213. }
  12214. }
  12215.  
  12216. clearInactiveVideoSources() {
  12217. const videoPlayers = this.inGallery ? this.getInactiveVideoPlayers() : this.videoPlayers;
  12218.  
  12219. for (const video of videoPlayers) {
  12220. video.src = "";
  12221. }
  12222. }
  12223.  
  12224. /**
  12225. * @param {HTMLVideoElement} video
  12226. * @returns {String | null}
  12227. */
  12228. getSourceIdFromVideo(video) {
  12229. const regex = /\.mp4\?(\d+)/;
  12230. const match = regex.exec(video.src);
  12231.  
  12232. if (match === null) {
  12233. return null;
  12234. }
  12235. return match[1];
  12236. }
  12237.  
  12238. /**
  12239. * @param {HTMLElement} thumb
  12240. */
  12241. playOriginalVideo(thumb) {
  12242. this.stopAllVideos();
  12243. const video = this.getActiveVideoPlayer();
  12244.  
  12245. this.setVideoSource(video, thumb);
  12246. video.style.display = "block";
  12247. video.play().catch(() => { });
  12248. this.toggleVideoControls(true);
  12249. }
  12250.  
  12251. stopAllVideos() {
  12252. for (const video of this.videoPlayers) {
  12253. this.stopVideo(video);
  12254. }
  12255. }
  12256.  
  12257. stopAllInactiveVideos() {
  12258. for (const video of this.getInactiveVideoPlayers()) {
  12259. this.stopVideo(video);
  12260. }
  12261. }
  12262.  
  12263. /**
  12264. * @param {HTMLVideoElement} video
  12265. */
  12266. stopVideo(video) {
  12267. video.style.display = "none";
  12268. video.pause();
  12269. video.removeAttribute("controls");
  12270. }
  12271.  
  12272. /**
  12273. * @param {HTMLElement} thumb
  12274. */
  12275. showOriginalGIF(thumb) {
  12276. const tags = Utils.getTagsFromThumb(thumb);
  12277. const extension = tags.has("animated_png") ? "png" : "gif";
  12278. const originalSource = Utils.getOriginalImageURLFromThumb(thumb).replace("jpg", extension);
  12279.  
  12280. this.gifContainer.src = originalSource;
  12281.  
  12282. if (this.showOriginalContentOnHover) {
  12283. this.toggleOriginalGIF(true);
  12284. this.lowResolutionCanvas.style.visibility = "hidden";
  12285. this.mainCanvas.style.visibility = "hidden";
  12286. this.gifContainer.style.visibility = "visible";
  12287. }
  12288. }
  12289.  
  12290. /**
  12291. * @param {HTMLElement} thumb
  12292. */
  12293. showOriginalImage(thumb) {
  12294. if (this.renderIsCompleted(thumb)) {
  12295. this.clearLowResolutionCanvas();
  12296. this.drawMainCanvas(thumb);
  12297. } else if (this.renderHasStarted(thumb)) {
  12298. this.drawLowResolutionCanvas(thumb);
  12299. this.clearMainCanvas();
  12300. this.drawMainCanvas(thumb);
  12301. } else {
  12302. this.drawLowResolutionCanvas(thumb);
  12303. this.renderOriginalImage(thumb);
  12304.  
  12305. if (!this.inGallery && !Gallery.settings.renderAroundAggressively) {
  12306. this.renderImagesAround(thumb);
  12307. }
  12308. }
  12309. this.toggleOriginalContentVisibility(this.showOriginalContentOnHover);
  12310. this.toggleOriginalGIF(false);
  12311. }
  12312.  
  12313. /**
  12314. * @param {HTMLElement} initialThumb
  12315. */
  12316. renderImagesAround(initialThumb) {
  12317. if (Utils.onSearchPage() || (Utils.onMobileDevice() && !this.enlargeOnClickOnMobile)) {
  12318. return;
  12319. }
  12320. this.renderImages(this.getAdjacentImageThumbs(initialThumb));
  12321. }
  12322.  
  12323. /**
  12324. * @param {HTMLElement} initialThumb
  12325. * @returns {HTMLElement[]}
  12326. */
  12327. getAdjacentImageThumbs(initialThumb) {
  12328. const adjacentImageThumbs = Utils.isImage(initialThumb) ? [initialThumb] : [];
  12329.  
  12330. if (Gallery.settings.loopAtEndOfGallery || this.latestSearchResults.length === 0) {
  12331. return adjacentImageThumbs.concat(this.getAdjacentImageThumbsOnCurrentPage(initialThumb));
  12332. }
  12333. return adjacentImageThumbs.concat(this.getAdjacentImageThumbsThroughoutAllPages(initialThumb));
  12334. }
  12335.  
  12336. /**
  12337. * @param {HTMLElement} initialThumb
  12338. * @returns {HTMLElement[]}
  12339. */
  12340. getAdjacentImageThumbsOnCurrentPage(initialThumb) {
  12341. return this.getAdjacentThumbsLooped(
  12342. initialThumb,
  12343. Gallery.settings.maxImagesToRenderAround,
  12344. (thumb) => {
  12345. return Utils.isImage(thumb);
  12346. }
  12347. );
  12348. }
  12349.  
  12350. /**
  12351. * @param {HTMLElement} initialThumb
  12352. * @returns {HTMLElement[]}
  12353. */
  12354. getAdjacentImageThumbsThroughoutAllPages(initialThumb) {
  12355. return this.getAdjacentSearchResults(
  12356. initialThumb,
  12357. Gallery.settings.maxImagesToRenderAround,
  12358. (post) => {
  12359. return Utils.isImage(post);
  12360. }
  12361. );
  12362. }
  12363.  
  12364. /**
  12365. * @param {HTMLElement} initialThumb
  12366. * @param {Number} limit
  12367. * @param {Function} qualifier
  12368. * @returns {HTMLElement[]}
  12369. */
  12370. getAdjacentThumbs(initialThumb, limit, qualifier) {
  12371. const adjacentThumbs = [];
  12372. let currentThumb = initialThumb;
  12373. let previousThumb = initialThumb;
  12374. let nextThumb = initialThumb;
  12375. let traverseForward = true;
  12376.  
  12377. while (currentThumb !== null && adjacentThumbs.length < limit) {
  12378. if (traverseForward) {
  12379. nextThumb = this.getAdjacentThumb(nextThumb, true);
  12380. } else {
  12381. previousThumb = this.getAdjacentThumb(previousThumb, false);
  12382. }
  12383. traverseForward = this.getTraversalDirection(previousThumb, traverseForward, nextThumb);
  12384. currentThumb = traverseForward ? nextThumb : previousThumb;
  12385.  
  12386. if (currentThumb !== null) {
  12387. if (qualifier(currentThumb)) {
  12388. adjacentThumbs.push(currentThumb);
  12389. }
  12390. }
  12391. }
  12392. return adjacentThumbs;
  12393. }
  12394.  
  12395. /**
  12396. * @param {HTMLElement} initialThumb
  12397. * @param {Number} limit
  12398. * @param {Function} additionalQualifier
  12399. * @returns {HTMLElement[]}
  12400. */
  12401. getAdjacentThumbsLooped(initialThumb, limit, additionalQualifier) {
  12402. const adjacentThumbs = [];
  12403. const discoveredIds = new Set();
  12404. let currentThumb = initialThumb;
  12405. let previousThumb = initialThumb;
  12406. let nextThumb = initialThumb;
  12407. let traverseForward = true;
  12408.  
  12409. while (currentThumb !== null && adjacentThumbs.length < limit) {
  12410. if (traverseForward) {
  12411. nextThumb = this.getAdjacentThumbLooped(nextThumb, true);
  12412. } else {
  12413. previousThumb = this.getAdjacentThumbLooped(previousThumb, false);
  12414. }
  12415. currentThumb = traverseForward ? nextThumb : previousThumb;
  12416. traverseForward = !traverseForward;
  12417.  
  12418. if (currentThumb === undefined || discoveredIds.has(currentThumb.id)) {
  12419. break;
  12420. }
  12421. discoveredIds.add(currentThumb.id);
  12422.  
  12423. if (additionalQualifier(currentThumb)) {
  12424. adjacentThumbs.push(currentThumb);
  12425. }
  12426. }
  12427. return adjacentThumbs;
  12428. }
  12429.  
  12430. /**
  12431. * @param {HTMLElement} previousThumb
  12432. * @param {HTMLElement} traverseForward
  12433. * @param {HTMLElement} nextThumb
  12434. * @returns {Boolean}
  12435. */
  12436. getTraversalDirection(previousThumb, traverseForward, nextThumb) {
  12437. if (previousThumb === null) {
  12438. traverseForward = true;
  12439. } else if (nextThumb === null) {
  12440. traverseForward = false;
  12441. }
  12442. return !traverseForward;
  12443. }
  12444.  
  12445. /**
  12446. * @param {HTMLElement} thumb
  12447. * @param {Boolean} forward
  12448. * @returns {HTMLElement}
  12449. */
  12450. getAdjacentThumbLooped(thumb, forward) {
  12451. let adjacentThumb = this.getAdjacentThumb(thumb, forward);
  12452.  
  12453. if (adjacentThumb === null) {
  12454. adjacentThumb = forward ? this.visibleThumbs[0] : this.visibleThumbs[this.visibleThumbs.length - 1];
  12455. }
  12456. return adjacentThumb;
  12457. }
  12458.  
  12459. /**
  12460. * @param {HTMLElement} thumb
  12461. * @param {Boolean} forward
  12462. * @returns {HTMLElement}
  12463. */
  12464. getAdjacentThumb(thumb, forward) {
  12465. return forward ? thumb.nextElementSibling : thumb.previousElementSibling;
  12466. }
  12467.  
  12468. /**
  12469. * @param {HTMLElement} initialThumb
  12470. * @param {Number} limit
  12471. * @param {Function} additionalQualifier
  12472. * @returns {HTMLElement[]}
  12473. */
  12474. getAdjacentSearchResults(initialThumb, limit, additionalQualifier) {
  12475. const initialSearchResultIndex = this.latestSearchResults.findIndex(post => post.id === initialThumb.id);
  12476.  
  12477. if (initialSearchResultIndex === -1) {
  12478. return [];
  12479. }
  12480. const adjacentSearchResults = [];
  12481. const discoveredIds = new Set();
  12482.  
  12483. let currentSearchResult;
  12484. let currentIndex;
  12485. let forward = true;
  12486. let previousIndex = initialSearchResultIndex;
  12487. let nextIndex = initialSearchResultIndex;
  12488.  
  12489. while (adjacentSearchResults.length < limit) {
  12490. if (forward) {
  12491. nextIndex = this.getAdjacentSearchResultIndex(nextIndex, true);
  12492. currentIndex = nextIndex;
  12493. forward = false;
  12494. } else {
  12495. previousIndex = this.getAdjacentSearchResultIndex(previousIndex, false);
  12496. currentIndex = previousIndex;
  12497. forward = true;
  12498. }
  12499. currentSearchResult = this.latestSearchResults[currentIndex];
  12500.  
  12501. if (discoveredIds.has(currentSearchResult.id)) {
  12502. break;
  12503. }
  12504. discoveredIds.add(currentSearchResult.id);
  12505.  
  12506. if (additionalQualifier(currentSearchResult)) {
  12507. adjacentSearchResults.push(currentSearchResult);
  12508. }
  12509. }
  12510.  
  12511. for (const searchResult of adjacentSearchResults) {
  12512. searchResult.activateHTMLElement();
  12513. }
  12514. return adjacentSearchResults.map(post => post.root);
  12515. }
  12516.  
  12517. /**
  12518. * @param {Number} i
  12519. * @param {Boolean} forward
  12520. * @returns {Number}
  12521. */
  12522. getAdjacentSearchResultIndex(i, forward) {
  12523. if (forward) {
  12524. i += 1;
  12525. i = i >= this.latestSearchResults.length ? 0 : i;
  12526. } else {
  12527. i -= 1;
  12528. i = i < 0 ? this.latestSearchResults.length - 1 : i;
  12529. }
  12530. return i;
  12531. }
  12532.  
  12533. /**
  12534. * @param {HTMLElement} thumb
  12535. * @returns {Boolean}
  12536. */
  12537. renderHasStarted(thumb) {
  12538. return this.startedRenders.has(thumb.id);
  12539. }
  12540.  
  12541. /**
  12542. * @param {HTMLElement} thumb
  12543. * @returns {Boolean}
  12544. */
  12545. renderIsCompleted(thumb) {
  12546. return this.completedRenders.has(thumb.id);
  12547. }
  12548.  
  12549. /**
  12550. * @param {HTMLElement} thumb
  12551. * @returns {Boolean}
  12552. */
  12553. canvasIsTransferrable(thumb) {
  12554. return !Utils.onMobileDevice() && !Utils.onSearchPage() && !this.transferredCanvases.has(thumb.id) && document.getElementById(thumb.id) !== null;
  12555. }
  12556.  
  12557. /**
  12558. * @param {HTMLElement} thumb
  12559. * @returns {{
  12560. * action: String,
  12561. * imageURL: String,
  12562. * id: String,
  12563. * extension: String,
  12564. * fetchDelay: Number,
  12565. * thumbURL: String,
  12566. * pixelCount: Number,
  12567. * canvas: OffscreenCanvas
  12568. * resolutionFraction: Number
  12569. * windowDimensions: {width: Number, height:Number}
  12570. * }}
  12571. */
  12572. getRenderRequest(thumb) {
  12573. const request = {
  12574. action: "render",
  12575. imageURL: Utils.getOriginalImageURLFromThumb(thumb),
  12576. id: thumb.id,
  12577. extension: Utils.getImageExtension(thumb.id),
  12578. fetchDelay: this.getBaseImageFetchDelay(thumb.id),
  12579. thumbURL: Utils.getImageFromThumb(thumb).src.replace("us.rule", "rule"),
  12580. pixelCount: this.getPixelCount(thumb),
  12581. resolutionFraction: Gallery.settings.upscaledThumbResolutionFraction
  12582. };
  12583.  
  12584. this.startedRenders.add(thumb.id);
  12585.  
  12586. if (this.canvasIsTransferrable(thumb)) {
  12587. request.canvas = this.getOffscreenCanvasFromThumb(thumb);
  12588. }
  12589.  
  12590. if (Utils.onMobileDevice()) {
  12591. request.windowDimensions = {
  12592. width: window.innerWidth,
  12593. height: window.innerHeight
  12594. };
  12595. }
  12596. return request;
  12597. }
  12598.  
  12599. /**
  12600. * @param {HTMLElement} thumb
  12601. * @returns {Number}
  12602. */
  12603. getPixelCount(thumb) {
  12604. if (Utils.onSearchPage()) {
  12605. return 0;
  12606. }
  12607. const defaultPixelCount = 2073600;
  12608. const pixelCount = Post.getPixelCount(thumb.id);
  12609. return pixelCount === 0 ? defaultPixelCount : pixelCount;
  12610. }
  12611.  
  12612. /**
  12613. * @param {HTMLElement} thumb
  12614. */
  12615. renderOriginalImage(thumb) {
  12616. if (Utils.onSearchPage()) {
  12617. return;
  12618. }
  12619.  
  12620. if (this.canvasIsTransferrable(thumb)) {
  12621. const request = this.getRenderRequest(thumb);
  12622.  
  12623. this.imageRenderer.postMessage(request, [request.canvas]);
  12624. } else {
  12625. this.imageRenderer.postMessage(this.getRenderRequest(thumb));
  12626. }
  12627. }
  12628.  
  12629. /**
  12630. * @param {HTMLElement} thumb
  12631. */
  12632. drawMainCanvas(thumb) {
  12633. this.imageRenderer.postMessage({
  12634. action: "drawMainCanvas",
  12635. id: thumb.id
  12636. });
  12637. }
  12638.  
  12639. clearMainCanvas() {
  12640. this.imageRenderer.postMessage({
  12641. action: "clearMainCanvas"
  12642. });
  12643. }
  12644.  
  12645. /**
  12646. * @param {Boolean} value
  12647. */
  12648. toggleOriginalContentVisibility(value) {
  12649. this.toggleMainCanvas(value);
  12650. this.toggleOriginalGIF(value);
  12651.  
  12652. if (!value) {
  12653. this.toggleOriginalVideoContainer(false);
  12654. }
  12655. }
  12656.  
  12657. /**
  12658. * @param {Boolean} value
  12659. */
  12660. toggleBackgroundVisibility(value) {
  12661. if (value === undefined) {
  12662. value = this.background.style.display === "block";
  12663. this.background.style.display = value ? "none" : "block";
  12664. this.originalImageLinkMask.style.display = value ? "none" : "block";
  12665. return;
  12666. }
  12667. this.background.style.display = value ? "block" : "none";
  12668. this.originalImageLinkMask.style.display = value ? "block" : "none";
  12669. }
  12670.  
  12671. /**
  12672. * @param {Boolean} value
  12673. */
  12674. toggleBackgroundOpacity(value) {
  12675. if (value !== undefined) {
  12676. if (value) {
  12677. this.updateBackgroundOpacity(1);
  12678. } else {
  12679. this.updateBackgroundOpacity(0);
  12680. }
  12681. return;
  12682. }
  12683. const opacity = parseFloat(this.background.style.opacity);
  12684.  
  12685. if (opacity < 1) {
  12686. this.updateBackgroundOpacity(1);
  12687. } else {
  12688. this.updateBackgroundOpacity(0);
  12689. }
  12690. }
  12691.  
  12692. /**
  12693. * @param {Boolean} value
  12694. */
  12695. toggleScrollbarVisibility(value) {
  12696. if (value === undefined) {
  12697. document.body.style.overflowY = document.body.style.overflowY === "auto" ? "hidden" : "auto";
  12698. return;
  12699. }
  12700. document.body.style.overflowY = value ? "auto" : "hidden";
  12701. }
  12702.  
  12703. /**
  12704. * @param {Boolean} value
  12705. */
  12706. toggleCursorVisibility(value) {
  12707. // const html = `
  12708. // #original-content-background {
  12709. // cursor: ${value ? "auto" : "none"};
  12710. // }
  12711. // `;
  12712.  
  12713. // insertStyleHTML(html, "gallery-cursor-visibility");
  12714. }
  12715.  
  12716. /**
  12717. * @param {Boolean} value
  12718. */
  12719. toggleVideoControls(value) {
  12720. const video = this.getActiveVideoPlayer();
  12721.  
  12722. if (Utils.onMobileDevice()) {
  12723. if (value) {
  12724. video.setAttribute("controls", "");
  12725. }
  12726. } else {
  12727. video.style.pointerEvents = value ? "auto" : "none";
  12728. }
  12729.  
  12730. if (!value) {
  12731. video.removeAttribute("controls");
  12732. }
  12733. }
  12734.  
  12735. /**
  12736. * @param {Boolean} value
  12737. */
  12738. toggleMainCanvas(value) {
  12739. if (value === undefined) {
  12740. this.mainCanvas.style.visibility = this.mainCanvas.style.visibility === "visible" ? "hidden" : "visible";
  12741. this.lowResolutionCanvas.style.visibility = this.mainCanvas.style.visibility === "visible" ? "hidden" : "visible";
  12742. } else {
  12743. this.mainCanvas.style.visibility = value ? "visible" : "hidden";
  12744. this.lowResolutionCanvas.style.visibility = value ? "visible" : "hidden";
  12745. }
  12746. }
  12747.  
  12748. /**
  12749. * @param {Boolean} value
  12750. */
  12751. toggleOriginalVideoContainer(value) {
  12752. if (value !== undefined) {
  12753. this.videoContainer.style.display = value ? "block" : "none";
  12754. return;
  12755. }
  12756.  
  12757. if (!this.currentlyHoveringOverVideoThumb() || this.videoContainer.style.display === "block") {
  12758. this.videoContainer.style.display = "none";
  12759. } else {
  12760. this.videoContainer.style.display = "block";
  12761. }
  12762. }
  12763.  
  12764. /**
  12765. * @param {HTMLElement} thumb
  12766. */
  12767. setActiveVideoPlayer(thumb) {
  12768. for (const video of this.videoPlayers) {
  12769. video.removeAttribute("active");
  12770. }
  12771.  
  12772. for (const video of this.videoPlayers) {
  12773. if (this.videoPlayerHasSource(video, thumb)) {
  12774. video.setAttribute("active", "");
  12775. return;
  12776. }
  12777. }
  12778. this.videoPlayers[0].setAttribute("active", "");
  12779. }
  12780.  
  12781. /**
  12782. * @returns {HTMLVideoElement}
  12783. */
  12784. getActiveVideoPlayer() {
  12785. return this.videoPlayers.find(video => video.hasAttribute("active")) || this.videoPlayers[0];
  12786. }
  12787.  
  12788. /**
  12789. * @param {HTMLVideoElement} video
  12790. * @param {HTMLElement} thumb
  12791. * @returns {Boolean}
  12792. */
  12793. videoPlayerHasSource(video, thumb) {
  12794. return video.src === this.getVideoSource(thumb);
  12795. }
  12796.  
  12797. /**
  12798. * @returns {HTMLVideoElement[]}
  12799. */
  12800. getInactiveVideoPlayers() {
  12801. return this.videoPlayers.filter(video => !video.hasAttribute("active"));
  12802. }
  12803.  
  12804. /**
  12805. * @param {Boolean} value
  12806. */
  12807. toggleOriginalGIF(value) {
  12808. if (value === undefined) {
  12809. value = this.gifContainer.style.visibility !== "visible";
  12810. }
  12811. this.gifContainer.style.visibility = value ? "visible" : "hidden";
  12812.  
  12813. if (Utils.onMobileDevice()) {
  12814. this.gifContainer.style.zIndex = value ? "9995" : "0";
  12815. }
  12816. }
  12817.  
  12818. /**
  12819. * @returns {Number}
  12820. */
  12821. getIndexOfThumbUnderCursor() {
  12822. return this.thumbUnderCursor === null ? null : this.getIndexFromThumb(this.thumbUnderCursor);
  12823. }
  12824.  
  12825. /**
  12826. * @returns {HTMLElement}
  12827. */
  12828. getSelectedThumb() {
  12829. return this.visibleThumbs[this.currentlySelectedThumbIndex];
  12830. }
  12831.  
  12832. /**
  12833. * @param {HTMLElement[]} animatedThumbs
  12834. */
  12835. upscaleAnimatedThumbs(animatedThumbs) {
  12836. if (Utils.onMobileDevice()) {
  12837. return;
  12838. }
  12839. const upscaleRequests = [];
  12840.  
  12841. for (const thumb of animatedThumbs) {
  12842. if (!this.canvasIsTransferrable(thumb)) {
  12843. continue;
  12844. }
  12845. let imageURL = Utils.getOriginalImageURL(Utils.getImageFromThumb(thumb).src);
  12846.  
  12847. if (Utils.isGif(thumb)) {
  12848. imageURL = imageURL.replace("jpg", "gif");
  12849. }
  12850. upscaleRequests.push({
  12851. id: thumb.id,
  12852. imageURL,
  12853. canvas: this.getOffscreenCanvasFromThumb(thumb),
  12854. resolutionFraction: Gallery.settings.upscaledAnimatedThumbResolutionFraction
  12855. });
  12856. }
  12857.  
  12858. this.imageRenderer.postMessage({
  12859. action: "upscaleAnimatedThumbs",
  12860. upscaleRequests
  12861. }, upscaleRequests.map(request => request.canvas));
  12862. }
  12863.  
  12864. /**
  12865. * @param {String} id
  12866. * @returns {Number}
  12867. */
  12868. getBaseImageFetchDelay(id) {
  12869. if (Utils.onFavoritesPage() && !Gallery.finishedLoading) {
  12870. return Gallery.settings.throttledImageFetchDelay;
  12871. }
  12872.  
  12873. if (Utils.extensionIsKnown(id)) {
  12874. return Gallery.settings.imageFetchDelayWhenExtensionKnown;
  12875. }
  12876. return Gallery.settings.imageFetchDelay;
  12877. }
  12878.  
  12879. /**
  12880. * @param {HTMLElement} thumb
  12881. */
  12882. upscaleAnimatedThumbsAround(thumb) {
  12883. if (!Utils.onFavoritesPage() || Utils.onMobileDevice()) {
  12884. return;
  12885. }
  12886. const animatedThumbsToUpscale = this.getAdjacentThumbs(thumb, Gallery.settings.animatedThumbsToUpscaleRange, (t) => {
  12887. return !Utils.isImage(t) && !this.transferredCanvases.has(t.id);
  12888. });
  12889.  
  12890. this.upscaleAnimatedThumbs(animatedThumbsToUpscale);
  12891. }
  12892.  
  12893. /**
  12894. * @param {HTMLElement} thumb
  12895. */
  12896. upscaleAnimatedThumbsAroundDiscrete(thumb) {
  12897. if (!Utils.onFavoritesPage() || Utils.onMobileDevice()) {
  12898. return;
  12899. }
  12900. const animatedThumbsToUpscale = this.getAdjacentThumbs(thumb, Gallery.settings.animatedThumbsToUpscaleDiscrete, (_) => {
  12901. return true;
  12902. }).filter(t => !Utils.isImage(t) && !this.transferredCanvases.has(t.id));
  12903.  
  12904. this.upscaleAnimatedThumbs(animatedThumbsToUpscale);
  12905. }
  12906.  
  12907. /**
  12908. * @param {Post[]} thumbs
  12909. * @returns {String[]}
  12910. */
  12911. getIdsWithUnknownExtensions(thumbs) {
  12912. return thumbs
  12913. .filter(thumb => Utils.isImage(thumb) && !Utils.extensionIsKnown(thumb.id))
  12914. .map(thumb => thumb.id);
  12915. }
  12916.  
  12917. /**
  12918. * @param {String} id
  12919. */
  12920. drawLowResolutionCanvas(thumb) {
  12921. const image = Utils.getImageFromThumb(thumb);
  12922.  
  12923. if (!Utils.imageIsLoaded(image)) {
  12924. return;
  12925. }
  12926. const ratio = Math.min(this.lowResolutionCanvas.width / image.naturalWidth, this.lowResolutionCanvas.height / image.naturalHeight);
  12927. const centerShiftX = (this.lowResolutionCanvas.width - (image.naturalWidth * ratio)) / 2;
  12928. const centerShiftY = (this.lowResolutionCanvas.height - (image.naturalHeight * ratio)) / 2;
  12929.  
  12930. this.clearLowResolutionCanvas();
  12931. this.lowResolutionContext.drawImage(
  12932. image, 0, 0, image.naturalWidth, image.naturalHeight,
  12933. centerShiftX, centerShiftY, image.naturalWidth * ratio, image.naturalHeight * ratio
  12934. );
  12935. }
  12936.  
  12937. clearLowResolutionCanvas() {
  12938. this.lowResolutionContext.clearRect(0, 0, this.lowResolutionCanvas.width, this.lowResolutionCanvas.height);
  12939. }
  12940.  
  12941. /**
  12942. * @param {Boolean} value
  12943. */
  12944. toggleVideoLooping(value) {
  12945. for (const video of this.videoPlayers) {
  12946. video.toggleAttribute("loop", value);
  12947. }
  12948. }
  12949.  
  12950. loadVideoClips() {
  12951. window.addEventListener("postProcess", () => {
  12952. setTimeout(() => {
  12953. let storedVideoClips;
  12954.  
  12955. try {
  12956. storedVideoClips = JSON.parse(localStorage.getItem("storedVideoClips") || "{}");
  12957.  
  12958. for (const [id, videoClip] of Object.entries(storedVideoClips)) {
  12959. this.videoClips.set(id, new VideoClip(videoClip));
  12960. }
  12961. } catch (error) {
  12962. console.error(error);
  12963. }
  12964. }, 50);
  12965. });
  12966. }
  12967.  
  12968. /**
  12969. * @param {KeyboardEvent} event
  12970. */
  12971. async addFavoriteInGallery(event) {
  12972. if (!this.inGallery || event.repeat || !Gallery.addOrRemoveFavoriteCooldown.ready) {
  12973. return;
  12974. }
  12975. const selectedThumb = this.getSelectedThumb();
  12976.  
  12977. if (selectedThumb === undefined || selectedThumb === null) {
  12978. Utils.showFullscreenIcon(Utils.icons.error);
  12979. return;
  12980. }
  12981. const addedFavoriteStatus = await Utils.addFavorite(selectedThumb.id);
  12982. let svg = Utils.icons.error;
  12983.  
  12984. switch (addedFavoriteStatus) {
  12985. case Utils.addedFavoriteStatuses.alreadyAdded:
  12986. svg = Utils.icons.heartCheck;
  12987. break;
  12988.  
  12989. case Utils.addedFavoriteStatuses.success:
  12990. svg = Utils.icons.heartPlus;
  12991. this.onFavoriteAddedOrDeleted(selectedThumb.id);
  12992. break;
  12993.  
  12994. default:
  12995. break;
  12996. }
  12997. Utils.showFullscreenIcon(svg);
  12998. }
  12999.  
  13000. /**
  13001. * @param {String} id
  13002. */
  13003. onFavoriteAddedOrDeleted(id) {
  13004. dispatchEvent(new CustomEvent("favoriteAddedOrDeleted", {
  13005. detail: id
  13006. }));
  13007. }
  13008.  
  13009. async setupOriginalImageLinkInGallery() {
  13010. const thumb = this.getSelectedThumb();
  13011.  
  13012. if (thumb === null || thumb === undefined) {
  13013. return;
  13014. }
  13015. const imageURL = await Utils.getOriginalImageURLWithExtension(thumb);
  13016.  
  13017. this.toggleCtrlClickOpenMediaInNewTab(false);
  13018. this.originalImageLinkMask.setAttribute("href", imageURL);
  13019. }
  13020.  
  13021. /**
  13022. * @param {Boolean} value
  13023. */
  13024. toggleCtrlClickOpenMediaInNewTab(value) {
  13025. if (!this.inGallery && value) {
  13026. return;
  13027. }
  13028. this.originalImageLinkMask.classList.toggle("active", value);
  13029. }
  13030. }
  13031.  
  13032. class Tooltip {
  13033. static tooltipHTML = `
  13034. <div id="tooltip-container">
  13035. <style>
  13036. #tooltip {
  13037. max-width: 750px;
  13038. border: 1px solid black;
  13039. padding: 0.25em;
  13040. position: absolute;
  13041. box-sizing: border-box;
  13042. z-index: 25;
  13043. pointer-events: none;
  13044. visibility: hidden;
  13045. opacity: 0;
  13046. transition: visibility 0s, opacity 0.25s linear;
  13047. font-size: 1.05em;
  13048. }
  13049.  
  13050. #tooltip.visible {
  13051. visibility: visible;
  13052. opacity: 1;
  13053. }
  13054. </style>
  13055. <span id="tooltip" class="light-green-gradient"></span>
  13056. </div>
  13057. `;
  13058. /**
  13059. * @type {Boolean}
  13060. */
  13061. static get disabled() {
  13062. return Utils.onMobileDevice() || Utils.getPerformanceProfile() > 1 || Utils.onPostPage();
  13063. }
  13064.  
  13065. /**
  13066. * @type {HTMLDivElement}
  13067. */
  13068. tooltip;
  13069. /**
  13070. * @type {String}
  13071. */
  13072. defaultTransition;
  13073. /**
  13074. * @type {Boolean}
  13075. */
  13076. visible;
  13077. /**
  13078. * @type {Object.<String,String>}
  13079. */
  13080. searchTagColorCodes;
  13081. /**
  13082. * @type {HTMLTextAreaElement}
  13083. */
  13084. searchBox;
  13085. /**
  13086. * @type {String}
  13087. */
  13088. previousSearch;
  13089. /**
  13090. * @type {HTMLImageElement}
  13091. */
  13092. currentImage;
  13093.  
  13094. constructor() {
  13095. if (Tooltip.disabled) {
  13096. return;
  13097. }
  13098. this.visible = Utils.getPreference("showTooltip", true);
  13099. Utils.insertFavoritesSearchGalleryHTML("afterbegin", Tooltip.tooltipHTML);
  13100. this.tooltip = document.getElementById("tooltip");
  13101. this.defaultTransition = this.tooltip.style.transition;
  13102. this.searchTagColorCodes = {};
  13103. this.currentImage = null;
  13104. this.addEventListeners();
  13105. this.addFavoritesOptions();
  13106. this.assignColorsToMatchedTags();
  13107. }
  13108.  
  13109. addEventListeners() {
  13110. this.addAllPageEventListeners();
  13111. this.addSearchPageEventListeners();
  13112. this.addFavoritesPageEventListeners();
  13113. }
  13114.  
  13115. addAllPageEventListeners() {
  13116. document.addEventListener("keydown", (event) => {
  13117. if (event.key.toLowerCase() !== "t" || !Utils.isHotkeyEvent(event)) {
  13118. return;
  13119. }
  13120.  
  13121. if (Utils.onFavoritesPage()) {
  13122. const showTooltipsCheckbox = document.getElementById("show-tooltips-checkbox");
  13123.  
  13124. if (showTooltipsCheckbox !== null) {
  13125. showTooltipsCheckbox.click();
  13126.  
  13127. if (this.currentImage !== null) {
  13128. if (this.visible) {
  13129. this.show(this.currentImage);
  13130. } else {
  13131. this.hide();
  13132. }
  13133. }
  13134. }
  13135. } else if (Utils.onSearchPage()) {
  13136. this.toggleVisibility();
  13137.  
  13138. if (this.currentImage !== null) {
  13139. this.hide();
  13140. }
  13141. }
  13142. }, {
  13143. passive: true
  13144. });
  13145. }
  13146.  
  13147. addSearchPageEventListeners() {
  13148. if (!Utils.onSearchPage()) {
  13149. return;
  13150. }
  13151. window.addEventListener("load", () => {
  13152. this.addEventListenersToThumbs.bind(this)();
  13153. }, {
  13154. once: true,
  13155. passive: true
  13156. });
  13157. }
  13158.  
  13159. addFavoritesPageEventListeners() {
  13160. if (!Utils.onFavoritesPage()) {
  13161. return;
  13162. }
  13163. window.addEventListener("favoritesFetched", () => {
  13164. this.addEventListenersToThumbs.bind(this)();
  13165. });
  13166. window.addEventListener("favoritesLoaded", () => {
  13167. this.addEventListenersToThumbs.bind(this)();
  13168. }, {
  13169. once: true
  13170. });
  13171. window.addEventListener("changedPage", () => {
  13172. this.currentImage = null;
  13173. this.addEventListenersToThumbs.bind(this)();
  13174. });
  13175. window.addEventListener("newFavoritesFetchedOnReload", (event) => {
  13176. if (!event.detail.empty) {
  13177. this.addEventListenersToThumbs.bind(this)(event.detail.thumbs);
  13178. }
  13179. }, {
  13180. once: true
  13181. });
  13182. }
  13183.  
  13184. assignColorsToMatchedTags() {
  13185. if (Utils.onSearchPage()) {
  13186. this.assignColorsToMatchedTagsOnSearchPage();
  13187. } else {
  13188. this.searchBox = document.getElementById("favorites-search-box");
  13189. this.assignColorsToMatchedTagsOnFavoritesPage();
  13190. this.searchBox.addEventListener("input", () => {
  13191. this.assignColorsToMatchedTagsOnFavoritesPage();
  13192. });
  13193. window.addEventListener("searchStarted", () => {
  13194. this.assignColorsToMatchedTagsOnFavoritesPage();
  13195. });
  13196.  
  13197. }
  13198. }
  13199.  
  13200. /**
  13201. * @param {HTMLCollectionOf.<Element>} thumbs
  13202. */
  13203. addEventListenersToThumbs(thumbs) {
  13204. thumbs = thumbs === undefined ? Utils.getAllThumbs() : thumbs;
  13205.  
  13206. for (const thumb of thumbs) {
  13207. const image = Utils.getImageFromThumb(thumb);
  13208.  
  13209. if (image.onmouseenter !== null) {
  13210. continue;
  13211. }
  13212.  
  13213. image.onmouseenter = (event) => {
  13214. if (Utils.enteredOverCaptionTag(event)) {
  13215. return;
  13216. }
  13217. this.currentImage = image;
  13218.  
  13219. if (this.visible) {
  13220. this.show(image);
  13221. }
  13222. };
  13223. image.onmouseleave = (event) => {
  13224. if (!Utils.enteredOverCaptionTag(event)) {
  13225. this.currentImage = null;
  13226. this.hide();
  13227. }
  13228. };
  13229. }
  13230. }
  13231.  
  13232. /**
  13233. * @param {HTMLImageElement} image
  13234. */
  13235. setPosition(image) {
  13236. const fancyHoveringStyle = document.getElementById("fancy-image-hovering-fsg-style");
  13237. const imageChangesSizeOnHover = fancyHoveringStyle !== null && fancyHoveringStyle.textContent !== "";
  13238. let rect;
  13239.  
  13240. if (imageChangesSizeOnHover) {
  13241. const imageContainer = image.parentElement;
  13242. const sizeCalculationDiv = document.createElement("div");
  13243.  
  13244. sizeCalculationDiv.className = "size-calculation-div";
  13245. imageContainer.appendChild(sizeCalculationDiv);
  13246. rect = sizeCalculationDiv.getBoundingClientRect();
  13247. sizeCalculationDiv.remove();
  13248. } else {
  13249. rect = image.getBoundingClientRect();
  13250. }
  13251. const offset = 7;
  13252. let tooltipRect;
  13253.  
  13254. this.tooltip.style.top = `${rect.bottom + offset + window.scrollY}px`;
  13255. this.tooltip.style.left = `${rect.x - 3}px`;
  13256. this.tooltip.classList.toggle("visible", true);
  13257. tooltipRect = this.tooltip.getBoundingClientRect();
  13258. const toolTipIsClippedAtBottom = tooltipRect.bottom > window.innerHeight;
  13259.  
  13260. if (!toolTipIsClippedAtBottom) {
  13261. return;
  13262. }
  13263. this.tooltip.style.top = `${rect.top - tooltipRect.height + window.scrollY - offset}px`;
  13264. tooltipRect = this.tooltip.getBoundingClientRect();
  13265. const menu = document.getElementById("favorites-search-gallery-menu");
  13266. const elementAboveTooltip = menu === null ? document.getElementById("header") : menu;
  13267. const elementAboveTooltipRect = elementAboveTooltip.getBoundingClientRect();
  13268. const toolTipIsClippedAtTop = tooltipRect.top < elementAboveTooltipRect.bottom;
  13269.  
  13270. if (!toolTipIsClippedAtTop) {
  13271. return;
  13272. }
  13273. const tooltipIsLeftOfCenter = tooltipRect.left < (window.innerWidth / 2);
  13274.  
  13275. this.tooltip.style.top = `${rect.top + window.scrollY + (rect.height / 2) - offset}px`;
  13276.  
  13277. if (tooltipIsLeftOfCenter) {
  13278. this.tooltip.style.left = `${rect.right + offset}px`;
  13279. } else {
  13280. this.tooltip.style.left = `${rect.left - 750 - offset}px`;
  13281. }
  13282. }
  13283.  
  13284. /**
  13285. * @param {HTMLImageElement} image
  13286. */
  13287. show(image) {
  13288. this.tooltip.innerHTML = this.formatHTML(this.getTags(image));
  13289. this.setPosition(image);
  13290. }
  13291.  
  13292. hide() {
  13293. this.tooltip.style.transition = "none";
  13294. this.tooltip.classList.toggle("visible", false);
  13295. setTimeout(() => {
  13296. this.tooltip.style.transition = this.defaultTransition;
  13297. }, 5);
  13298. }
  13299.  
  13300. /**
  13301. * @param {HTMLImageElement} image
  13302. * @returns {String}
  13303. */
  13304. getTags(image) {
  13305. const thumb = Utils.getThumbFromImage(image);
  13306. const tags = Utils.getTagsFromThumb(thumb);
  13307.  
  13308. if (this.searchTagColorCodes[thumb.id] === undefined) {
  13309. tags.delete(thumb.id);
  13310. }
  13311. return Array.from(tags).sort().join(" ");
  13312. }
  13313.  
  13314. /**
  13315. * @returns {String}
  13316. */
  13317. getRandomColor() {
  13318. const letters = "0123456789ABCDEF";
  13319. let color = "#";
  13320.  
  13321. for (let i = 0; i < 6; i += 1) {
  13322. if (i === 2 || i === 3) {
  13323. color += "0";
  13324. } else {
  13325. color += letters[Math.floor(Math.random() * letters.length)];
  13326. }
  13327. }
  13328. return color;
  13329. }
  13330.  
  13331. /**
  13332. * @param {String} tags
  13333. */
  13334. formatHTML(tags) {
  13335. let unmatchedTagsHTML = "";
  13336. let matchedTagsHTML = "";
  13337.  
  13338. const tagList = Utils.removeExtraWhiteSpace(tags).split(" ");
  13339.  
  13340. for (let i = 0; i < tagList.length; i += 1) {
  13341. const tag = tagList[i];
  13342. const tagColor = this.getColorCode(tag);
  13343. const tagWithSpace = `${tag} `;
  13344.  
  13345. if (tagColor !== undefined) {
  13346. matchedTagsHTML += `<span style="color:${tagColor}"><b>${tagWithSpace}</b></span>`;
  13347. } else if (Utils.includesTag(tag, new Set(Utils.tagBlacklist.split(" ")))) {
  13348. unmatchedTagsHTML += `<span style="color:red"><s><b>${tagWithSpace}</b></s></span>`;
  13349. } else {
  13350. unmatchedTagsHTML += tagWithSpace;
  13351. }
  13352. }
  13353. const html = matchedTagsHTML + unmatchedTagsHTML;
  13354.  
  13355. if (html === "") {
  13356. return tags;
  13357. }
  13358. return html;
  13359. }
  13360.  
  13361. /**
  13362. * @param {String} searchQuery
  13363. */
  13364. assignTagColors(searchQuery) {
  13365. searchQuery = this.removeNotTags(searchQuery);
  13366. const {orGroups, remainingSearchTags} = Utils.extractTagGroups(searchQuery);
  13367.  
  13368. this.searchTagColorCodes = {};
  13369. this.assignColorsToOrGroupTags(orGroups);
  13370. this.assignColorsToRemainingTags(remainingSearchTags);
  13371. }
  13372.  
  13373. /**
  13374. * @param {String[][]} orGroups
  13375. */
  13376. assignColorsToOrGroupTags(orGroups) {
  13377.  
  13378. for (const orGroup of orGroups) {
  13379. const color = this.getRandomColor();
  13380.  
  13381. for (const tag of orGroup) {
  13382. this.addColorCodedTag(tag, color);
  13383. }
  13384. }
  13385. }
  13386.  
  13387. /**
  13388. * @param {String[]} remainingTags
  13389. */
  13390. assignColorsToRemainingTags(remainingTags) {
  13391. for (const tag of remainingTags) {
  13392. this.addColorCodedTag(tag, this.getRandomColor());
  13393. }
  13394. }
  13395.  
  13396. /**
  13397. * @param {String} tags
  13398. * @returns {String}
  13399. */
  13400. removeNotTags(tags) {
  13401. return tags.replace(/(?:^| )-\S+/gm, "");
  13402. }
  13403.  
  13404. sanitizeTags(tags) {
  13405. return tags.toLowerCase().trim();
  13406. }
  13407.  
  13408. addColorCodedTag(tag, color) {
  13409. tag = this.sanitizeTags(tag);
  13410.  
  13411. if (this.searchTagColorCodes[tag] === undefined) {
  13412. this.searchTagColorCodes[tag] = color;
  13413. }
  13414. }
  13415.  
  13416. /**
  13417. * @param {String} tag
  13418. * @returns {String | null}
  13419. */
  13420. getColorCode(tag) {
  13421. if (this.searchTagColorCodes[tag] !== undefined) {
  13422. return this.searchTagColorCodes[tag];
  13423. }
  13424.  
  13425. for (const searchTag of Object.keys(this.searchTagColorCodes)) {
  13426. if (Utils.tagsMatchWildcardSearchTag(searchTag, [tag])) {
  13427. return this.searchTagColorCodes[searchTag];
  13428. }
  13429. }
  13430. return undefined;
  13431. }
  13432.  
  13433. addFavoritesOptions() {
  13434. Utils.createFavoritesOption(
  13435. "show-tooltips",
  13436. " Tooltips",
  13437. "Show tags when hovering over a thumbnail and see which ones were matched by a search",
  13438. this.visible, (event) => {
  13439. this.toggleVisibility(event.target.checked);
  13440. },
  13441. true,
  13442. "(T)"
  13443. );
  13444. }
  13445.  
  13446. /**
  13447. * @param {Boolean} value
  13448. */
  13449. toggleVisibility(value) {
  13450. if (value === undefined) {
  13451. value = !this.visible;
  13452. }
  13453. Utils.setPreference("showTooltip", value);
  13454. this.visible = value;
  13455. }
  13456.  
  13457. /**
  13458. * @param {HTMLElement | null} thumb
  13459. */
  13460. showOnLoadIfHoveringOverThumb(thumb) {
  13461. if (thumb !== null) {
  13462. this.show(Utils.getImageFromThumb(thumb));
  13463. }
  13464. }
  13465.  
  13466. assignColorsToMatchedTagsOnSearchPage() {
  13467. const searchQuery = document.getElementsByName("tags")[0].getAttribute("value");
  13468.  
  13469. this.assignTagColors(searchQuery);
  13470. }
  13471.  
  13472. assignColorsToMatchedTagsOnFavoritesPage() {
  13473. if (this.searchBox.value === this.previousSearch) {
  13474. return;
  13475. }
  13476. this.previousSearch = this.searchBox.value;
  13477. this.assignTagColors(this.searchBox.value);
  13478. }
  13479. }
  13480.  
  13481. class SavedSearches {
  13482. static savedSearchesHTML = `
  13483. <div id="saved-searches">
  13484. <style>
  13485. #saved-searches-container {
  13486. margin: 0;
  13487. display: flex;
  13488. flex-direction: column;
  13489. padding: 0;
  13490. }
  13491.  
  13492. #saved-searches-input-container {
  13493. margin-bottom: 10px;
  13494. }
  13495.  
  13496. #saved-searches-input {
  13497. flex: 15 1 auto;
  13498. margin-right: 10px;
  13499. }
  13500.  
  13501. #savedSearches {
  13502. max-width: 100%;
  13503.  
  13504. button {
  13505. flex: 1 1 auto;
  13506. cursor: pointer;
  13507. }
  13508. }
  13509.  
  13510. #saved-searches-buttons button {
  13511. margin-right: 1px;
  13512. margin-bottom: 5px;
  13513. border: none;
  13514. border-radius: 4px;
  13515. cursor: pointer;
  13516. height: 35px;
  13517.  
  13518. &:hover {
  13519. filter: brightness(140%);
  13520. }
  13521. }
  13522.  
  13523. #saved-search-list-container {
  13524. direction: rtl;
  13525. max-height: 200px;
  13526. overflow-y: auto;
  13527. overflow-x: hidden;
  13528. margin: 0;
  13529. padding: 0;
  13530. }
  13531.  
  13532. #saved-search-list {
  13533. direction: ltr;
  13534. >li {
  13535. display: flex;
  13536. flex-direction: row;
  13537. cursor: pointer;
  13538. background: rgba(0, 0, 0, .1);
  13539.  
  13540. &:nth-child(odd) {
  13541. background: rgba(0, 0, 0, 0.2);
  13542. }
  13543.  
  13544. >div {
  13545. padding: 4px;
  13546. align-content: center;
  13547.  
  13548. svg {
  13549. height: 20px;
  13550. width: 20px;
  13551. }
  13552. }
  13553. }
  13554. }
  13555.  
  13556. .save-search-label {
  13557. flex: 1000 30px;
  13558. text-align: left;
  13559.  
  13560. &:hover {
  13561. color: white;
  13562. background: #0075FF;
  13563. }
  13564. }
  13565.  
  13566. .edit-saved-search-button {
  13567. text-align: center;
  13568. flex: 1 20px;
  13569.  
  13570. &:hover {
  13571. color: white;
  13572. background: slategray;
  13573. }
  13574. }
  13575.  
  13576. .remove-saved-search-button {
  13577. text-align: center;
  13578. flex: 1 20px;
  13579.  
  13580. &:hover {
  13581. color: white;
  13582. background: #f44336;
  13583. }
  13584. }
  13585.  
  13586. .move-saved-search-to-top-button {
  13587. text-align: center;
  13588.  
  13589. &:hover {
  13590. color: white;
  13591. background: steelblue;
  13592. }
  13593. }
  13594.  
  13595. /* .tag-type-saved>a,
  13596. .tag-type-saved {
  13597. color: lightblue;
  13598. } */
  13599. </style>
  13600. <h2>Saved Searches</h2>
  13601. <div id="saved-searches-buttons">
  13602. <button title="Save custom search" id="save-custom-search-button">Save</button>
  13603. <button id="stop-editing-saved-search-button" style="display: none;">Cancel</button>
  13604. <span>
  13605. <button title="Export all saved searches" id="export-saved-search-button">Export</button>
  13606. <button title="Import saved searches" id="import-saved-search-button">Import</button>
  13607. </span>
  13608. <button title="Save result ids as search" id="save-results-button">Save Results</button>
  13609. </div>
  13610. <div id="saved-searches-container">
  13611. <div id="saved-searches-input-container">
  13612. <textarea id="saved-searches-input" spellcheck="false" style="width: 97%;"
  13613. placeholder="Save Custom Search"></textarea>
  13614. </div>
  13615. <div id="saved-search-list-container">
  13616. <ul id="saved-search-list"></ul>
  13617. </div>
  13618. </div>
  13619. </div>
  13620. <script>
  13621. </script>
  13622. `;
  13623. static preferences = {
  13624. textareaWidth: "savedSearchesTextAreaWidth",
  13625. textareaHeight: "savedSearchesTextAreaHeight",
  13626. savedSearches: "savedSearches",
  13627. visibility: "savedSearchVisibility",
  13628. tutorial: "savedSearchesTutorial"
  13629. };
  13630. static localStorageKeys = {
  13631. savedSearches: "savedSearches"
  13632. };
  13633. /**
  13634. * @type {Boolean}
  13635. */
  13636. static get disabled() {
  13637. return !Utils.onFavoritesPage() || Utils.onMobileDevice();
  13638. }
  13639. /**
  13640. * @type {HTMLTextAreaElement}
  13641. */
  13642. textarea;
  13643. /**
  13644. * @type {HTMLElement}
  13645. */
  13646. savedSearchesList;
  13647. /**
  13648. * @type {HTMLButtonElement}
  13649. */
  13650. stopEditingButton;
  13651. /**
  13652. * @type {HTMLButtonElement}
  13653. */
  13654. saveButton;
  13655. /**
  13656. * @type {HTMLButtonElement}
  13657. */
  13658. importButton;
  13659. /**
  13660. * @type {HTMLButtonElement}
  13661. */
  13662. exportButton;
  13663. /**
  13664. * @type {HTMLButtonElement}
  13665. */
  13666. saveSearchResultsButton;
  13667.  
  13668. constructor() {
  13669. if (SavedSearches.disabled) {
  13670. return;
  13671. }
  13672. this.insertHTML();
  13673. this.extractHTMLElements();
  13674. this.addEventListeners();
  13675. this.loadSavedSearches();
  13676. }
  13677.  
  13678. insertHTML() {
  13679. const showSavedSearches = Utils.getPreference(SavedSearches.preferences.visibility, false);
  13680. const savedSearchesContainer = document.getElementById("right-favorites-panel");
  13681.  
  13682. Utils.insertHTMLAndExtractStyle(savedSearchesContainer, "beforeend", SavedSearches.savedSearchesHTML);
  13683. document.getElementById("right-favorites-panel").style.display = showSavedSearches ? "block" : "none";
  13684. const options = Utils.createFavoritesOption(
  13685. "show-saved-searches",
  13686. "Saved Searches",
  13687. "Toggle saved searches",
  13688. showSavedSearches,
  13689. (e) => {
  13690. savedSearchesContainer.style.display = e.target.checked ? "block" : "none";
  13691. Utils.setPreference(SavedSearches.preferences.visibility, e.target.checked);
  13692. },
  13693. true
  13694. );
  13695.  
  13696. document.getElementById("bottom-panel-2").insertAdjacentElement("afterbegin", options);
  13697. }
  13698.  
  13699. extractHTMLElements() {
  13700. this.saveButton = document.getElementById("save-custom-search-button");
  13701. this.textarea = document.getElementById("saved-searches-input");
  13702. this.savedSearchesList = document.getElementById("saved-search-list");
  13703. this.stopEditingButton = document.getElementById("stop-editing-saved-search-button");
  13704. this.importButton = document.getElementById("import-saved-search-button");
  13705. this.exportButton = document.getElementById("export-saved-search-button");
  13706. this.saveSearchResultsButton = document.getElementById("save-results-button");
  13707. }
  13708.  
  13709. addEventListeners() {
  13710. this.saveButton.onclick = () => {
  13711. this.saveSearch(this.textarea.value.trim());
  13712. };
  13713. this.textarea.addEventListener("keydown", (event) => {
  13714. switch (event.key) {
  13715. case "Enter":
  13716. if (Utils.awesompleteIsUnselected(this.textarea)) {
  13717. event.preventDefault();
  13718. this.saveButton.click();
  13719. this.textarea.blur();
  13720. setTimeout(() => {
  13721. this.textarea.focus();
  13722. }, 100);
  13723. }
  13724. break;
  13725.  
  13726. case "Escape":
  13727. if (Utils.awesompleteIsUnselected(this.textarea) && this.stopEditingButton.style.display === "block") {
  13728. this.stopEditingButton.click();
  13729. }
  13730. break;
  13731.  
  13732. default:
  13733. break;
  13734. }
  13735. }, {
  13736. passive: true
  13737. });
  13738. this.exportButton.onclick = () => {
  13739. this.exportSavedSearches();
  13740. };
  13741. this.importButton.onclick = () => {
  13742. this.importSavedSearches();
  13743. };
  13744. this.saveSearchResultsButton.onclick = () => {
  13745. this.saveSearchResultsAsCustomSearch();
  13746. };
  13747. }
  13748.  
  13749. /**
  13750. * @param {String} newSavedSearch
  13751. * @param {Boolean} updateLocalStorage
  13752. */
  13753. saveSearch(newSavedSearch, updateLocalStorage = true) {
  13754. if (newSavedSearch === "" || newSavedSearch === undefined) {
  13755. return;
  13756. }
  13757. const newListItem = document.createElement("li");
  13758. const savedSearchLabel = document.createElement("div");
  13759. const editButton = document.createElement("div");
  13760. const removeButton = document.createElement("div");
  13761. const moveToTopButton = document.createElement("div");
  13762.  
  13763. savedSearchLabel.innerText = newSavedSearch;
  13764. editButton.innerHTML = Utils.icons.edit;
  13765. removeButton.innerHTML = Utils.icons.delete;
  13766. moveToTopButton.innerHTML = Utils.icons.upArrow;
  13767. editButton.title = "Edit";
  13768. removeButton.title = "Delete";
  13769. moveToTopButton.title = "Move to top";
  13770. savedSearchLabel.className = "save-search-label";
  13771. editButton.className = "edit-saved-search-button";
  13772. removeButton.className = "remove-saved-search-button";
  13773. moveToTopButton.className = "move-saved-search-to-top-button";
  13774. newListItem.appendChild(removeButton);
  13775. newListItem.appendChild(editButton);
  13776. newListItem.appendChild(moveToTopButton);
  13777. newListItem.appendChild(savedSearchLabel);
  13778. this.savedSearchesList.insertBefore(newListItem, this.savedSearchesList.firstChild);
  13779. savedSearchLabel.onclick = () => {
  13780. const searchBox = document.getElementById("favorites-search-box");
  13781.  
  13782. navigator.clipboard.writeText(savedSearchLabel.innerText);
  13783.  
  13784. if (searchBox === null) {
  13785. return;
  13786. }
  13787.  
  13788. if (searchBox.value !== "") {
  13789. searchBox.value += " ";
  13790. }
  13791. searchBox.value += savedSearchLabel.innerText;
  13792. };
  13793. removeButton.onclick = () => {
  13794. if (this.inEditMode()) {
  13795. alert("Cancel current edit before removing another search");
  13796. return;
  13797. }
  13798.  
  13799. if (confirm(`Remove saved search: ${savedSearchLabel.innerText} ?`)) {
  13800. this.savedSearchesList.removeChild(newListItem);
  13801. this.storeSavedSearches();
  13802. }
  13803. };
  13804. editButton.onclick = () => {
  13805. if (this.inEditMode()) {
  13806. alert("Cancel current edit before editing another search");
  13807. } else {
  13808. this.editSavedSearches(savedSearchLabel, newListItem);
  13809. }
  13810. };
  13811. moveToTopButton.onclick = () => {
  13812. if (this.inEditMode()) {
  13813. alert("Cancel current edit before moving this search to the top");
  13814. return;
  13815. }
  13816. newListItem.parentElement.insertAdjacentElement("afterbegin", newListItem);
  13817. this.storeSavedSearches();
  13818. };
  13819. this.stopEditingButton.onclick = () => {
  13820. this.stopEditingSavedSearches(newListItem);
  13821. };
  13822. this.textarea.value = "";
  13823.  
  13824. if (updateLocalStorage) {
  13825. this.storeSavedSearches();
  13826. }
  13827. }
  13828.  
  13829. /**
  13830. * @param {HTMLLabelElement} savedSearchLabel
  13831. */
  13832. editSavedSearches(savedSearchLabel) {
  13833. this.textarea.value = savedSearchLabel.innerText;
  13834. this.saveButton.textContent = "Save Changes";
  13835. this.textarea.focus();
  13836. this.exportButton.style.display = "none";
  13837. this.importButton.style.display = "none";
  13838. this.stopEditingButton.style.display = "";
  13839. this.saveButton.onclick = () => {
  13840. savedSearchLabel.innerText = this.textarea.value.trim();
  13841. this.storeSavedSearches();
  13842. this.stopEditingButton.click();
  13843. };
  13844. }
  13845.  
  13846. /**
  13847. * @param {HTMLElement} newListItem
  13848. */
  13849. stopEditingSavedSearches(newListItem) {
  13850. this.saveButton.textContent = "Save";
  13851. this.saveButton.onclick = () => {
  13852. this.saveSearch(this.textarea.value.trim());
  13853. };
  13854. this.textarea.value = "";
  13855. this.exportButton.style.display = "";
  13856. this.importButton.style.display = "";
  13857. this.stopEditingButton.style.display = "none";
  13858. newListItem.style.border = "";
  13859. }
  13860.  
  13861. storeSavedSearches() {
  13862. localStorage.setItem(SavedSearches.localStorageKeys.savedSearches, JSON.stringify(Utils.getSavedSearchValues()));
  13863. }
  13864.  
  13865. loadSavedSearches() {
  13866. const savedSearches = JSON.parse(localStorage.getItem(SavedSearches.localStorageKeys.savedSearches)) || [];
  13867. const firstUse = Utils.getPreference(SavedSearches.preferences.tutorial, true);
  13868.  
  13869. Utils.setPreference(SavedSearches.preferences.tutorial, false);
  13870.  
  13871. if (firstUse && savedSearches.length === 0) {
  13872. this.createTutorialSearches();
  13873. return;
  13874. }
  13875.  
  13876. for (let i = savedSearches.length - 1; i >= 0; i -= 1) {
  13877. this.saveSearch(savedSearches[i], false);
  13878. }
  13879. }
  13880.  
  13881. createTutorialSearches() {
  13882. const searches = [];
  13883.  
  13884. window.addEventListener("startedFetchingFavorites", async() => {
  13885. await Utils.sleep(1000);
  13886. const postIds = Utils.getAllThumbs().map(thumb => thumb.id);
  13887.  
  13888. Utils.shuffleArray(postIds);
  13889.  
  13890. const exampleSearch = `( EXAMPLE: ~ ${postIds.slice(0, 9).join(" ~ ")} ) ( male* ~ female* ~ 1boy ~ 1girls )`;
  13891.  
  13892. searches.push(exampleSearch);
  13893.  
  13894. for (let i = searches.length - 1; i >= 0; i -= 1) {
  13895. this.saveSearch(searches[i]);
  13896. }
  13897. }, {
  13898. once: true
  13899. });
  13900. }
  13901.  
  13902. /**
  13903. * @returns {Boolean}
  13904. */
  13905. inEditMode() {
  13906. return this.stopEditingButton.style.display !== "none";
  13907. }
  13908.  
  13909. exportSavedSearches() {
  13910. const savedSearchString = Array.from(document.getElementsByClassName("save-search-label")).map(search => search.innerText).join("\n");
  13911.  
  13912. navigator.clipboard.writeText(savedSearchString);
  13913. alert("Copied saved searches to clipboard");
  13914. }
  13915.  
  13916. importSavedSearches() {
  13917. const doesNotHaveSavedSearches = this.savedSearchesList.querySelectorAll("li").length === 0;
  13918.  
  13919. if (doesNotHaveSavedSearches || confirm("Are you sure you want to import saved searches? This will overwrite current saved searches.")) {
  13920. const savedSearches = this.textarea.value.split("\n");
  13921.  
  13922. this.savedSearchesList.innerHTML = "";
  13923.  
  13924. for (let i = savedSearches.length - 1; i >= 0; i -= 1) {
  13925. this.saveSearch(savedSearches[i]);
  13926. }
  13927. this.storeSavedSearches();
  13928. }
  13929. }
  13930.  
  13931. saveSearchResultsAsCustomSearch() {
  13932. const searchResultIds = Array.from(Post.allPosts.values())
  13933. .filter(post => post.matchedByMostRecentSearch)
  13934. .map(post => post.id);
  13935.  
  13936. if (searchResultIds.length === 0) {
  13937. return;
  13938. }
  13939.  
  13940. if (searchResultIds.length > 300) {
  13941. if (!confirm(`Are you sure you want to save ${searchResultIds.length} ids as one search?`)) {
  13942. return;
  13943. }
  13944. }
  13945. const customSearch = `( ${searchResultIds.join(" ~ ")} )`;
  13946.  
  13947. this.saveSearch(customSearch);
  13948. }
  13949. }
  13950.  
  13951. class Caption {
  13952. static captionHTML = `
  13953. <style>
  13954. .caption {
  13955. overflow: hidden;
  13956. pointer-events: none;
  13957. background: rgba(0, 0, 0, .75);
  13958. z-index: 15;
  13959. position: absolute;
  13960. width: 100%;
  13961. height: 100%;
  13962. top: -100%;
  13963. left: 0px;
  13964. top: 0px;
  13965. text-align: left;
  13966. transform: translateX(-100%);
  13967. /* transition: transform .3s cubic-bezier(.26,.28,.2,.82); */
  13968. transition: transform .35s ease;
  13969. padding-top: 0.5ch;
  13970. padding-left: 7px;
  13971.  
  13972. h6 {
  13973. display: block;
  13974. color: white;
  13975. padding-top: 0px;
  13976. }
  13977.  
  13978. li {
  13979. width: fit-content;
  13980. list-style-type: none;
  13981. display: inline-block;
  13982. }
  13983.  
  13984. &.active {
  13985. transform: translateX(0%);
  13986. }
  13987.  
  13988. &.transition-completed {
  13989. .caption-tag {
  13990. pointer-events: all;
  13991. }
  13992. }
  13993. }
  13994.  
  13995. .caption.hide {
  13996. display: none;
  13997. }
  13998.  
  13999. .caption.inactive {
  14000. display: none;
  14001. }
  14002.  
  14003. .caption-tag {
  14004. pointer-events: none;
  14005. color: #6cb0ff;
  14006. word-wrap: break-word;
  14007.  
  14008. &:hover {
  14009. text-decoration-line: underline;
  14010. cursor: pointer;
  14011. }
  14012. }
  14013.  
  14014. .artist-tag {
  14015. color: #f0a0a0;
  14016. }
  14017.  
  14018. .character-tag {
  14019. color: #f0f0a0;
  14020. }
  14021.  
  14022. .copyright-tag {
  14023. color: #EFA1CF;
  14024. }
  14025.  
  14026. .metadata-tag {
  14027. color: #8FD9ED;
  14028. }
  14029.  
  14030. .caption-wrapper {
  14031. pointer-events: none;
  14032. position: absolute !important;
  14033. overflow: hidden;
  14034. top: -1px;
  14035. left: -1px;
  14036. width: 102%;
  14037. height: 102%;
  14038. display: block !important;
  14039. }
  14040. </style>
  14041. `;
  14042. static preferences = {
  14043. visibility: "showCaptions"
  14044. };
  14045. static localStorageKeys = {
  14046. tagCategories: "tagCategories"
  14047. };
  14048. static importantTagCategories = new Set([
  14049. "copyright",
  14050. "character",
  14051. "artist",
  14052. "metadata"
  14053. ]);
  14054. static tagCategoryEncodings = {
  14055. 0: "general",
  14056. 1: "artist",
  14057. 2: "unknown",
  14058. 3: "copyright",
  14059. 4: "character",
  14060. 5: "metadata"
  14061. };
  14062. static template = `
  14063. <ul id="caption-list">
  14064. <li id="caption-id" style="display: block;"><h6>ID</h6></li>
  14065. ${Caption.getCategoryHeaderHTML()}
  14066. </ul>
  14067. `;
  14068. static findCategoriesOnPageChangeCooldown = new Cooldown(3000, true);
  14069. static saveTagCategoriesCooldown = new Cooldown(1000);
  14070. /**
  14071. * @type {Object.<String, Number>}
  14072. */
  14073. static tagCategoryAssociations;
  14074. static settings = {
  14075. tagFetchDelayAfterFinishedLoading: 10,
  14076. tagFetchDelayBeforeFinishedLoading: 100
  14077. };
  14078. static flags = {
  14079. finishedLoading: false
  14080. };
  14081.  
  14082. /**
  14083. * @returns {String}
  14084. */
  14085. static getCategoryHeaderHTML() {
  14086. let html = "";
  14087.  
  14088. for (const category of Caption.importantTagCategories) {
  14089. const capitalizedCategory = Utils.capitalize(category);
  14090. const header = capitalizedCategory === "Metadata" ? "Meta" : capitalizedCategory;
  14091.  
  14092. html += `<li id="caption${capitalizedCategory}" style="display: none;"><h6>${header}</h6></li>`;
  14093. }
  14094. return html;
  14095. }
  14096.  
  14097. /**
  14098. * @param {String} tagCategory
  14099. * @returns {Number}
  14100. */
  14101. static encodeTagCategory(tagCategory) {
  14102. for (const [encoding, category] of Object.entries(Caption.tagCategoryEncodings)) {
  14103. if (category === tagCategory) {
  14104. return parseInt(encoding);
  14105. }
  14106. }
  14107. return 0;
  14108. }
  14109.  
  14110. /**
  14111. * @type {Boolean}
  14112. */
  14113. static get disabled() {
  14114. return !Utils.onFavoritesPage() || Utils.onMobileDevice() || Utils.getPerformanceProfile() > 1;
  14115. }
  14116.  
  14117. /**
  14118. * @type {Boolean}
  14119. */
  14120. get hidden() {
  14121. return this.caption.classList.contains("hide") || this.caption.classList.contains("disabled") || this.caption.classList.contains("remove");
  14122. }
  14123.  
  14124. /**
  14125. * @type {Number}
  14126. */
  14127. static get tagFetchDelay() {
  14128. if (Caption.flags.finishedLoading) {
  14129. return Caption.settings.tagFetchDelayAfterFinishedLoading;
  14130. }
  14131. return Caption.settings.tagFetchDelayBeforeFinishedLoading;
  14132. }
  14133.  
  14134. /**
  14135. * @type {HTMLDivElement}
  14136. */
  14137. captionWrapper;
  14138. /**
  14139. * @type {HTMLDivElement}
  14140. */
  14141. caption;
  14142. /**
  14143. * @type {HTMLElement}
  14144. */
  14145. currentThumb;
  14146. /**
  14147. * @type {Set.<String>}
  14148. */
  14149. problematicTags;
  14150. /**
  14151. * @type {String}
  14152. */
  14153. currentThumbId;
  14154. /**
  14155. * @type {AbortController}
  14156. */
  14157. abortController;
  14158.  
  14159. constructor() {
  14160. if (Caption.disabled) {
  14161. return;
  14162. }
  14163. this.initializeFields();
  14164. this.createHTMLElement();
  14165. this.insertHTML();
  14166. this.toggleVisibility(this.getVisibilityPreference());
  14167. this.addEventListeners();
  14168. }
  14169.  
  14170. initializeFields() {
  14171. Caption.tagCategoryAssociations = this.loadSavedTags();
  14172. Caption.findCategoriesOnPageChangeCooldown.onDebounceEnd = () => {
  14173. this.findTagCategoriesOnPageChange();
  14174. };
  14175. Caption.saveTagCategoriesCooldown.onCooldownEnd = () => {
  14176. this.saveTagCategories();
  14177. };
  14178. this.currentThumb = null;
  14179. this.problematicTags = new Set();
  14180. this.currentThumbId = null;
  14181. this.abortController = new AbortController();
  14182. }
  14183.  
  14184. createHTMLElement() {
  14185. this.captionWrapper = document.createElement("div");
  14186. this.captionWrapper.className = "caption-wrapper";
  14187. this.caption = document.createElement("div");
  14188. this.caption.className = "caption inactive not-highlightable";
  14189. this.captionWrapper.appendChild(this.caption);
  14190. document.head.appendChild(this.captionWrapper);
  14191. this.caption.innerHTML = Caption.template;
  14192. }
  14193.  
  14194. insertHTML() {
  14195. Utils.insertStyleHTML(Caption.captionHTML, "caption");
  14196. Utils.createFavoritesOption(
  14197. "show-captions",
  14198. "Details",
  14199. "Show details when hovering over thumbnail",
  14200. this.getVisibilityPreference(),
  14201. (event) => {
  14202. this.toggleVisibility(event.target.checked);
  14203. },
  14204. true,
  14205. "(D)"
  14206. );
  14207. }
  14208.  
  14209. /**
  14210. * @param {Boolean} value
  14211. */
  14212. toggleVisibility(value) {
  14213. if (value === undefined) {
  14214. value = this.caption.classList.contains("disabled");
  14215. }
  14216.  
  14217. if (value) {
  14218. this.caption.classList.remove("disabled");
  14219. } else if (!this.caption.classList.contains("disabled")) {
  14220. this.caption.classList.add("disabled");
  14221. }
  14222. Utils.setPreference(Caption.preferences.visibility, value);
  14223. }
  14224.  
  14225. addEventListeners() {
  14226. this.addAllPageEventListeners();
  14227. this.addSearchPageEventListeners();
  14228. this.addFavoritesPageEventListeners();
  14229. }
  14230.  
  14231. addAllPageEventListeners() {
  14232. this.caption.addEventListener("transitionend", () => {
  14233. if (this.caption.classList.contains("active")) {
  14234. this.caption.classList.add("transition-completed");
  14235. }
  14236. this.caption.classList.remove("transitioning");
  14237. });
  14238. this.caption.addEventListener("transitionstart", () => {
  14239. this.caption.classList.add("transitioning");
  14240. });
  14241. window.addEventListener("showOriginalContent", (event) => {
  14242. const thumb = this.caption.parentElement;
  14243.  
  14244. if (event.detail) {
  14245. this.removeFromThumb(thumb);
  14246.  
  14247. this.caption.classList.add("hide");
  14248. } else {
  14249. this.caption.classList.remove("hide");
  14250. }
  14251. });
  14252.  
  14253. document.addEventListener("keydown", (event) => {
  14254. if (event.key.toLowerCase() !== "d" || !Utils.isHotkeyEvent(event)) {
  14255. return;
  14256. }
  14257.  
  14258. if (Utils.onFavoritesPage()) {
  14259. const showCaptionsCheckbox = document.getElementById("show-captions-checkbox");
  14260.  
  14261. if (showCaptionsCheckbox !== null) {
  14262. showCaptionsCheckbox.click();
  14263.  
  14264. if (this.currentThumb !== null && !this.caption.classList.contains("remove")) {
  14265. if (showCaptionsCheckbox.checked) {
  14266. this.attachToThumbHelper(this.currentThumb);
  14267. } else {
  14268. this.removeFromThumbHelper(this.currentThumb);
  14269. }
  14270. }
  14271. }
  14272. } else if (Utils.onSearchPage()) {
  14273. // this.toggleVisibility();
  14274. }
  14275. }, {
  14276. passive: true
  14277. });
  14278. }
  14279.  
  14280. addSearchPageEventListeners() {
  14281. if (!Utils.onSearchPage()) {
  14282. return;
  14283. }
  14284. window.addEventListener("load", () => {
  14285. this.addEventListenersToThumbs.bind(this)();
  14286. }, {
  14287. once: true,
  14288. passive: true
  14289. });
  14290. }
  14291.  
  14292. addFavoritesPageEventListeners() {
  14293. window.addEventListener("favoritesLoaded", () => {
  14294. this.addEventListenersToThumbs.bind(this)();
  14295. Caption.flags.finishedLoading = true;
  14296. Caption.findCategoriesOnPageChangeCooldown.waitTime = 1000;
  14297. }, {
  14298. once: true
  14299. });
  14300. window.addEventListener("favoritesLoadedFromDatabase", () => {
  14301. this.findTagCategoriesOnPageChange();
  14302. }, {
  14303. once: true
  14304. });
  14305. window.addEventListener("favoritesFetched", () => {
  14306. this.addEventListenersToThumbs.bind(this)();
  14307. });
  14308. window.addEventListener("changedPage", () => {
  14309. this.addEventListenersToThumbs.bind(this)();
  14310. this.abortController.abort("ChangedPage");
  14311. this.abortController = new AbortController();
  14312.  
  14313. if (Caption.findCategoriesOnPageChangeCooldown.ready) {
  14314. setTimeout(() => {
  14315. this.findTagCategoriesOnPageChange();
  14316. }, 100);
  14317. }
  14318. });
  14319. window.addEventListener("originalFavoritesCleared", (event) => {
  14320. const thumbs = event.detail;
  14321. const tagNames = Array.from(thumbs)
  14322. .map(thumb => Utils.getImageFromThumb(thumb).title)
  14323. .join(" ")
  14324. .split(" ")
  14325. .filter(tagName => !Utils.isNumber(tagName) && Caption.tagCategoryAssociations[tagName] === undefined);
  14326.  
  14327. this.findTagCategories(tagNames, () => {
  14328. Caption.saveTagCategoriesCooldown.restart();
  14329. });
  14330. }, {
  14331. once: true
  14332. });
  14333. window.addEventListener("newFavoritesFetchedOnReload", (event) => {
  14334. if (!event.detail.empty) {
  14335. this.addEventListenersToThumbs.bind(this)(event.detail.thumbs);
  14336. }
  14337. }, {
  14338. once: true
  14339. });
  14340. window.addEventListener("captionOverrideEnd", () => {
  14341. if (this.currentThumb !== null) {
  14342. this.attachToThumb(this.currentThumb);
  14343. }
  14344. });
  14345. }
  14346.  
  14347. /**
  14348. * @param {HTMLElement[]} thumbs
  14349. */
  14350. async addEventListenersToThumbs(thumbs) {
  14351. await Utils.sleep(500);
  14352. thumbs = thumbs === undefined ? Utils.getAllThumbs() : thumbs;
  14353.  
  14354. for (const thumb of thumbs) {
  14355. const imageContainer = Utils.getImageFromThumb(thumb).parentElement;
  14356.  
  14357. if (imageContainer.onmouseenter !== null) {
  14358. continue;
  14359. }
  14360. imageContainer.onmouseenter = () => {
  14361. this.currentThumb = thumb;
  14362. this.attachToThumb(thumb);
  14363. };
  14364.  
  14365. imageContainer.onmouseleave = () => {
  14366. this.currentThumb = null;
  14367. this.removeFromThumb(thumb);
  14368. };
  14369. }
  14370. }
  14371.  
  14372. /**
  14373. * @param {HTMLElement} thumb
  14374. */
  14375. attachToThumb(thumb) {
  14376. if (this.hidden || thumb === null) {
  14377. return;
  14378. }
  14379. this.attachToThumbHelper(thumb);
  14380. }
  14381.  
  14382. attachToThumbHelper(thumb) {
  14383. thumb.querySelectorAll(".caption-wrapper-clone").forEach(element => element.remove());
  14384. this.caption.classList.remove("inactive");
  14385. this.caption.innerHTML = Caption.template;
  14386. this.captionWrapper.removeAttribute("style");
  14387. const captionIdHeader = this.caption.querySelector("#caption-id");
  14388. const captionIdTag = document.createElement("li");
  14389.  
  14390. captionIdTag.className = "caption-tag";
  14391. captionIdTag.textContent = thumb.id;
  14392. captionIdTag.onclick = (event) => {
  14393. event.preventDefault();
  14394. event.stopPropagation();
  14395. };
  14396. captionIdTag.addEventListener("contextmenu", (event) => {
  14397. event.preventDefault();
  14398. event.stopPropagation();
  14399. });
  14400.  
  14401. captionIdTag.onmousedown = (event) => {
  14402. event.preventDefault();
  14403. event.stopPropagation();
  14404. this.tagOnClick(thumb.id, event);
  14405. };
  14406. captionIdHeader.insertAdjacentElement("afterend", captionIdTag);
  14407. thumb.children[0].appendChild(this.captionWrapper);
  14408. this.populateTags(thumb);
  14409. }
  14410.  
  14411. /**
  14412. * @param {HTMLElement} thumb
  14413. */
  14414. removeFromThumb(thumb) {
  14415. if (this.hidden) {
  14416. return;
  14417. }
  14418.  
  14419. this.removeFromThumbHelper(thumb);
  14420. }
  14421.  
  14422. /**
  14423. * @param {HTMLElement} thumb
  14424. */
  14425. removeFromThumbHelper(thumb) {
  14426. if (thumb !== null && thumb !== undefined) {
  14427. this.animateRemoval(thumb);
  14428. }
  14429. this.animate(false);
  14430. this.caption.classList.add("inactive");
  14431. this.caption.classList.remove("transition-completed");
  14432. }
  14433.  
  14434. /**
  14435. * @param {HTMLElement} thumb
  14436. */
  14437. animateRemoval(thumb) {
  14438. const captionWrapperClone = this.captionWrapper.cloneNode(true);
  14439. const captionClone = captionWrapperClone.children[0];
  14440.  
  14441. thumb.querySelectorAll(".caption-wrapper-clone").forEach(element => element.remove());
  14442. captionWrapperClone.classList.add("caption-wrapper-clone");
  14443. captionWrapperClone.querySelectorAll("*").forEach(element => element.removeAttribute("id"));
  14444. captionClone.ontransitionend = () => {
  14445. captionWrapperClone.remove();
  14446. };
  14447. thumb.children[0].appendChild(captionWrapperClone);
  14448. setTimeout(() => {
  14449. captionClone.classList.remove("active");
  14450. }, 4);
  14451. }
  14452.  
  14453. /**
  14454. * @param {HTMLElement} thumb
  14455. */
  14456. resizeFont(thumb) {
  14457. const columnInput = document.getElementById("column-resize-input");
  14458. const heightCanBeDerivedWithoutRect = this.thumbMetadataExists(thumb) && columnInput !== null;
  14459. let height;
  14460.  
  14461. if (heightCanBeDerivedWithoutRect) {
  14462. height = this.estimateThumbHeightFromMetadata(thumb, columnInput);
  14463. } else {
  14464. height = Utils.getImageFromThumb(thumb).getBoundingClientRect().height;
  14465. }
  14466. const captionListRect = this.caption.children[0].getBoundingClientRect();
  14467. const ratio = height / captionListRect.height;
  14468. const scale = ratio > 1 ? Math.sqrt(ratio) : ratio * 0.85;
  14469.  
  14470. this.caption.parentElement.style.fontSize = `${Utils.roundToTwoDecimalPlaces(scale)}em`;
  14471. }
  14472.  
  14473. /**
  14474. * @param {HTMLElement} thumb
  14475. * @returns {Boolean}
  14476. */
  14477. thumbMetadataExists(thumb) {
  14478. if (Utils.onSearchPage()) {
  14479. return false;
  14480. }
  14481. const post = Post.allPosts.get(thumb.id);
  14482.  
  14483. if (post === undefined) {
  14484. return false;
  14485. }
  14486.  
  14487. if (post.metadata === undefined) {
  14488. return false;
  14489. }
  14490.  
  14491. if (post.metadata.width <= 0 || post.metadata.width <= 0) {
  14492. return false;
  14493. }
  14494. return true;
  14495. }
  14496.  
  14497. /**
  14498. * @param {HTMLElement} thumb
  14499. * @param {HTMLInputElement} columnInput
  14500. * @returns {Number}
  14501. */
  14502. estimateThumbHeightFromMetadata(thumb, columnInput) {
  14503. const post = Post.allPosts.get(thumb.id);
  14504. const gridGap = 16;
  14505. const columnCount = Math.max(1, parseInt(columnInput.value));
  14506. const thumbWidthEstimate = (window.innerWidth - (columnCount * gridGap)) / columnCount;
  14507. const thumbWidthScale = post.metadata.width / thumbWidthEstimate;
  14508. return post.metadata.height / thumbWidthScale;
  14509. }
  14510.  
  14511. /**
  14512. * @param {String} tagCategory
  14513. * @param {String} tagName
  14514. */
  14515. addTag(tagCategory, tagName) {
  14516. if (!Caption.importantTagCategories.has(tagCategory)) {
  14517. return;
  14518. }
  14519. const header = document.getElementById(this.getCategoryHeaderId(tagCategory));
  14520. const tag = document.createElement("li");
  14521.  
  14522. tag.className = `${tagCategory}-tag caption-tag`;
  14523. tag.textContent = this.replaceUnderscoresWithSpaces(tagName);
  14524. header.insertAdjacentElement("afterend", tag);
  14525. header.style.display = "block";
  14526. tag.onmouseover = (event) => {
  14527. event.stopPropagation();
  14528. };
  14529. tag.onclick = (event) => {
  14530. event.stopPropagation();
  14531. event.preventDefault();
  14532. };
  14533. tag.addEventListener("contextmenu", (event) => {
  14534. event.preventDefault();
  14535. event.stopPropagation();
  14536. });
  14537. tag.onmousedown = (event) => {
  14538. event.preventDefault();
  14539. event.stopPropagation();
  14540. this.tagOnClick(tagName, event);
  14541. };
  14542. }
  14543.  
  14544. /**
  14545. * @returns {Object.<String, Number>}
  14546. */
  14547. loadSavedTags() {
  14548. return JSON.parse(localStorage.getItem(Caption.localStorageKeys.tagCategories) || "{}");
  14549. }
  14550.  
  14551. saveTagCategories() {
  14552. localStorage.setItem(Caption.localStorageKeys.tagCategories, JSON.stringify(Caption.tagCategoryAssociations));
  14553. }
  14554.  
  14555. /**
  14556. * @param {String} tagName
  14557. * @param {MouseEvent} event
  14558. */
  14559. tagOnClick(tagName, event) {
  14560. switch (event.button) {
  14561. case Utils.clickCodes.left:
  14562. if (event.shiftKey) {
  14563. this.searchForTag(tagName);
  14564. } else {
  14565. this.tagOnClickHelper(tagName, event);
  14566. }
  14567. break;
  14568.  
  14569. case Utils.clickCodes.middle:
  14570. this.searchForTag(tagName);
  14571. break;
  14572.  
  14573. case Utils.clickCodes.right:
  14574. this.tagOnClickHelper(`-${tagName}`, event);
  14575. break;
  14576.  
  14577. default:
  14578. break;
  14579. }
  14580. }
  14581.  
  14582. /**
  14583. * @param {String} tagName
  14584. */
  14585. searchForTag(tagName) {
  14586. dispatchEvent(new CustomEvent("searchForTag", {
  14587. detail: tagName
  14588. }));
  14589. }
  14590.  
  14591. /**
  14592. * @param {String} value
  14593. * @param {MouseEvent} mouseEvent
  14594. */
  14595. tagOnClickHelper(value, mouseEvent) {
  14596. if (mouseEvent.ctrlKey) {
  14597. Utils.openSearchPage(value);
  14598. return;
  14599. }
  14600. const searchBox = Utils.onSearchPage() ? document.getElementsByName("tags")[0] : document.getElementById("favorites-search-box");
  14601. const searchBoxDoesNotIncludeTag = true;
  14602.  
  14603. navigator.clipboard.writeText(value);
  14604.  
  14605. if (searchBoxDoesNotIncludeTag) {
  14606. searchBox.value += ` ${value}`;
  14607. searchBox.focus();
  14608. value = searchBox.value;
  14609. searchBox.value = "";
  14610. searchBox.value = value;
  14611. }
  14612. }
  14613.  
  14614. /**
  14615. * @param {String} tagName
  14616. * @returns {String}
  14617. */
  14618. replaceUnderscoresWithSpaces(tagName) {
  14619. return tagName.replaceAll(/_/gm, " ");
  14620. }
  14621.  
  14622. /**
  14623. * @param {String} tagName
  14624. * @returns {String}
  14625. */
  14626. replaceSpacesWithUnderscores(tagName) {
  14627. return tagName.replaceAll(/\s/gm, "_");
  14628. }
  14629.  
  14630. /**
  14631. * @returns {Boolean}
  14632. */
  14633. getVisibilityPreference() {
  14634. return Utils.getPreference(Caption.preferences.visibility, true);
  14635. }
  14636.  
  14637. /**
  14638. * @param {Boolean} value
  14639. */
  14640. animate(value) {
  14641. this.caption.classList.toggle("active", value);
  14642. }
  14643.  
  14644. /**
  14645. * @param {String} tagCategory
  14646. * @returns {String}
  14647. */
  14648. getCategoryHeaderId(tagCategory) {
  14649. return `caption${Utils.capitalize(tagCategory)}`;
  14650. }
  14651.  
  14652. /**
  14653. * @param {HTMLElement} thumb
  14654. */
  14655. populateTags(thumb) {
  14656. const tagNames = Utils.getTagsFromThumb(thumb);
  14657.  
  14658. tagNames.delete(thumb.id);
  14659. const unknownThumbTags = Array.from(tagNames)
  14660. .filter(tagName => this.tagCategoryIsUnknown(thumb, tagName));
  14661.  
  14662. this.currentThumbId = thumb.id;
  14663.  
  14664. if (this.allTagsAreProblematic(unknownThumbTags)) {
  14665. this.correctAllProblematicTagsFromThumb(thumb, () => {
  14666. this.addTags(tagNames, thumb);
  14667. });
  14668. return;
  14669. }
  14670.  
  14671. if (unknownThumbTags.length > 0) {
  14672. this.findTagCategories(unknownThumbTags, () => {
  14673. this.addTags(tagNames, thumb);
  14674. }, 3);
  14675. return;
  14676. }
  14677. this.addTags(tagNames, thumb);
  14678. }
  14679.  
  14680. /**
  14681. * @param {String[]} tagNames
  14682. * @param {HTMLElement} thumb
  14683. */
  14684. addTags(tagNames, thumb) {
  14685. Caption.saveTagCategoriesCooldown.restart();
  14686.  
  14687. if (this.currentThumbId !== thumb.id) {
  14688. return;
  14689. }
  14690.  
  14691. if (thumb.getElementsByClassName("caption-tag").length > 1) {
  14692. return;
  14693. }
  14694.  
  14695. for (const tagName of tagNames) {
  14696. const category = this.getTagCategory(tagName);
  14697.  
  14698. this.addTag(category, tagName);
  14699. }
  14700. this.resizeFont(thumb);
  14701. this.animate(true);
  14702. }
  14703.  
  14704. /**
  14705. * @param {String} tagName
  14706. * @returns {String}
  14707. */
  14708. getTagCategory(tagName) {
  14709. const encoding = Caption.tagCategoryAssociations[tagName];
  14710.  
  14711. if (encoding === undefined) {
  14712. return "general";
  14713. }
  14714. return Caption.tagCategoryEncodings[encoding];
  14715. }
  14716.  
  14717. /**
  14718. * @param {String[]} tags
  14719. * @returns {Boolean}
  14720. */
  14721. allTagsAreProblematic(tags) {
  14722. for (const tag of tags) {
  14723. if (!this.problematicTags.has(tag)) {
  14724. return false;
  14725. }
  14726. }
  14727. return tags.length > 0;
  14728. }
  14729.  
  14730. /**
  14731. * @param {HTMLElement} thumb
  14732. * @param {Function} onProblematicTagsCorrected
  14733. */
  14734. correctAllProblematicTagsFromThumb(thumb, onProblematicTagsCorrected) {
  14735. fetch(Utils.getPostPageURL(thumb.id))
  14736. .then((response) => {
  14737. return response.text();
  14738. })
  14739. .then((html) => {
  14740. const tagCategoryMap = this.getTagCategoryMapFromPostPage(html);
  14741.  
  14742. for (const [tagName, tagCategory] of tagCategoryMap.entries()) {
  14743. Caption.tagCategoryAssociations[tagName] = Caption.encodeTagCategory(tagCategory);
  14744. this.problematicTags.delete(tagName);
  14745. }
  14746. onProblematicTagsCorrected();
  14747. })
  14748. .catch((error) => {
  14749. console.error(error);
  14750. });
  14751. }
  14752.  
  14753. /**
  14754. * @param {String} html
  14755. * @returns {Map.<String, String>}
  14756. */
  14757. getTagCategoryMapFromPostPage(html) {
  14758. const dom = new DOMParser().parseFromString(html, "text/html");
  14759. return Array.from(dom.querySelectorAll(".tag"))
  14760. .reduce((map, element) => {
  14761. const tagCategory = element.classList[0].replace("tag-type-", "");
  14762. const tagName = this.replaceSpacesWithUnderscores(element.children[1].textContent);
  14763.  
  14764. map.set(tagName, tagCategory);
  14765. return map;
  14766. }, new Map());
  14767. }
  14768.  
  14769. /**
  14770. * @param {String} tag
  14771. */
  14772. setAsProblematic(tag) {
  14773. if (Caption.tagCategoryAssociations[tag] === undefined && !Utils.customTags.has(tag)) {
  14774. this.problematicTags.add(tag);
  14775. }
  14776. }
  14777.  
  14778. findTagCategoriesOnPageChange() {
  14779. const tagNames = this.getTagNamesWithUnknownCategories(Utils.getAllThumbs().slice(0, 200));
  14780.  
  14781. this.findTagCategories(tagNames, () => {
  14782. Caption.saveTagCategoriesCooldown.restart();
  14783. });
  14784. }
  14785.  
  14786. /**
  14787. * @param {String[]} tagNames
  14788. * @param {Function} onAllCategoriesFound
  14789. * @param {Number} fetchDelay
  14790. */
  14791. async findTagCategories(tagNames, onAllCategoriesFound, fetchDelay) {
  14792. const parser = new DOMParser();
  14793. const lastTagName = tagNames[tagNames.length - 1];
  14794. const uniqueTagNames = new Set(tagNames);
  14795.  
  14796. for (const tagName of uniqueTagNames) {
  14797. if (Utils.isNumber(tagName) && tagName.length > 5) {
  14798. Caption.tagCategoryAssociations[tagName] = 0;
  14799. continue;
  14800. }
  14801.  
  14802. if (tagName.includes("'")) {
  14803. this.setAsProblematic(tagName);
  14804. }
  14805.  
  14806. if (this.problematicTags.has(tagName)) {
  14807. if (tagName === lastTagName) {
  14808. onAllCategoriesFound();
  14809. }
  14810. continue;
  14811. }
  14812.  
  14813. const apiURL = `https://api.rule34.xxx//index.php?page=dapi&s=tag&q=index&name=${encodeURIComponent(tagName)}`;
  14814.  
  14815. try {
  14816. fetch(apiURL, {
  14817. signal: this.abortController.signal
  14818. })
  14819. .then((response) => {
  14820. if (response.ok) {
  14821. return response.text();
  14822. }
  14823. throw new Error(response.statusText);
  14824. })
  14825. .then((html) => {
  14826. const dom = parser.parseFromString(html, "text/html");
  14827. const encoding = dom.getElementsByTagName("tag")[0].getAttribute("type");
  14828.  
  14829. if (encoding === "array") {
  14830. this.setAsProblematic(tagName);
  14831. return;
  14832. }
  14833. Caption.tagCategoryAssociations[tagName] = parseInt(encoding);
  14834.  
  14835. if (tagName === lastTagName) {
  14836. onAllCategoriesFound();
  14837. }
  14838. }).catch(() => {
  14839. onAllCategoriesFound();
  14840. });
  14841. } catch (error) {
  14842. console.error(error);
  14843. }
  14844. await Utils.sleep(fetchDelay || Caption.tagFetchDelay);
  14845. }
  14846. }
  14847.  
  14848. /**
  14849. * @param {HTMLElement[]} thumbs
  14850. * @returns {String[]}
  14851. */
  14852. getTagNamesWithUnknownCategories(thumbs) {
  14853. const tagNamesWithUnknownCategories = new Set();
  14854.  
  14855. for (const thumb of thumbs) {
  14856. const tagNames = Array.from(Utils.getTagsFromThumb(thumb));
  14857.  
  14858. for (const tagName of tagNames) {
  14859. if (this.tagCategoryIsUnknown(thumb, tagName)) {
  14860. tagNamesWithUnknownCategories.add(tagName);
  14861. }
  14862. }
  14863. }
  14864. return Array.from(tagNamesWithUnknownCategories);
  14865. }
  14866.  
  14867. /**
  14868. * @param {HTMLElement} thumb
  14869. * @param {String} tagName
  14870. * @returns
  14871. */
  14872. tagCategoryIsUnknown(thumb, tagName) {
  14873. return tagName !== thumb.id && Caption.tagCategoryAssociations[tagName] === undefined && !Utils.customTags.has(tagName);
  14874. }
  14875. }
  14876.  
  14877. class TagModifier {
  14878. static tagModifierHTML = `
  14879. <div id="tag-modifier-container">
  14880. <style>
  14881. #tag-modifier-ui-container {
  14882. display: none;
  14883.  
  14884. >* {
  14885. margin-top: 10px;
  14886. }
  14887. }
  14888.  
  14889. #tag-modifier-ui-textarea {
  14890. width: 80%;
  14891. }
  14892.  
  14893. .favorite.tag-modifier-selected {
  14894. outline: 2px dashed white !important;
  14895.  
  14896. >div, >a {
  14897. opacity: 1;
  14898. filter: grayscale(0%);
  14899. }
  14900. }
  14901.  
  14902. #tag-modifier-ui-status-label {
  14903. visibility: hidden;
  14904. }
  14905.  
  14906. .tag-type-custom>a,
  14907. .tag-type-custom {
  14908. color: hotpink;
  14909. }
  14910. </style>
  14911. <div id="tag-modifier-option-container">
  14912. <label class="checkbox" title="Add or remove custom or official tags to favorites">
  14913. <input type="checkbox" id="tag-modifier-option-checkbox">Modify Tags<span class="option-hint"></span>
  14914. </label>
  14915. </div>
  14916. <div id="tag-modifier-ui-container">
  14917. <label id="tag-modifier-ui-status-label">No Status</label>
  14918. <textarea id="tag-modifier-ui-textarea" placeholder="tags" spellcheck="false"></textarea>
  14919. <div id="tag-modifier-buttons">
  14920. <span id="tag-modifier-ui-modification-buttons">
  14921. <button id="tag-modifier-ui-add" title="Add tags to selected favorites">Add</button>
  14922. <button id="tag-modifier-remove" title="Remove tags from selected favorites">Remove</button>
  14923. </span>
  14924. <span id="tag-modifier-ui-selection-buttons">
  14925. <button id="tag-modifier-ui-select-all" title="Select all favorites for tag modification">Select all</button>
  14926. <button id="tag-modifier-ui-un-select-all" title="Unselect all favorites for tag modification">Unselect
  14927. all</button>
  14928. </span>
  14929. </div>
  14930. <div id="tag-modifier-ui-reset-button-container">
  14931. <button id="tag-modifier-reset" title="Reset tag modifications">Reset</button>
  14932. </div>
  14933. <div id="tag-modifier-ui-configuration" style="display: none;">
  14934. <button id="tag-modifier-import" title="Import modified tags">Import</button>
  14935. <button id="tag-modifier-export" title="Export modified tags">Export</button>
  14936. </div>
  14937. </div>
  14938. </div>
  14939. `;
  14940. /**
  14941. * @type {String}
  14942. */
  14943. static databaseName = "AdditionalTags";
  14944. /**
  14945. * @type {String}
  14946. */
  14947. static objectStoreName = "additionalTags";
  14948. /**
  14949. * @type {Map.<String, String>}
  14950. */
  14951. static tagModifications = new Map();
  14952. static preferences = {
  14953. modifyTagsOutsideFavoritesPage: "modifyTagsOutsideFavoritesPage"
  14954. };
  14955.  
  14956. /**
  14957. * @type {Boolean}
  14958. */
  14959. static get currentlyModifyingTags() {
  14960. return document.getElementById("tag-edit-mode") !== null;
  14961. }
  14962.  
  14963. /**
  14964. * @type {Boolean}
  14965. */
  14966. static get disabled() {
  14967. if (Utils.onMobileDevice()) {
  14968. return true;
  14969. }
  14970.  
  14971. if (Utils.onFavoritesPage()) {
  14972. return false;
  14973. }
  14974. return Utils.getPreference(TagModifier.preferences.modifyTagsOutsideFavoritesPage, false);
  14975. }
  14976.  
  14977. /**
  14978. * @type {AbortController}
  14979. */
  14980. tagEditModeAbortController;
  14981. /**
  14982. * @type {{container: HTMLDivElement, checkbox: HTMLInputElement}}
  14983. */
  14984. favoritesOption;
  14985. /**
  14986. * @type { {container: HTMLDivElement,
  14987. * textarea: HTMLTextAreaElement,
  14988. * statusLabel: HTMLLabelElement,
  14989. * add: HTMLButtonElement,
  14990. * remove: HTMLButtonElement,
  14991. * reset: HTMLButtonElement,
  14992. * selectAll: HTMLButtonElement,
  14993. * unSelectAll: HTMLButtonElement,
  14994. * import: HTMLButtonElement,
  14995. * export: HTMLButtonElement}}
  14996. */
  14997. favoritesUI;
  14998. /**
  14999. * @type {Post[]}
  15000. */
  15001. selectedPosts;
  15002. /**
  15003. * @type {Boolean}
  15004. */
  15005. atLeastOneFavoriteIsSelected;
  15006.  
  15007. constructor() {
  15008. if (TagModifier.disabled) {
  15009. return;
  15010. }
  15011. this.tagEditModeAbortController = new AbortController();
  15012. this.favoritesOption = {};
  15013. this.favoritesUI = {};
  15014. this.selectedPosts = [];
  15015. this.atLeastOneFavoriteIsSelected = false;
  15016. this.loadTagModifications();
  15017. this.insertHTML();
  15018. this.addEventListeners();
  15019. }
  15020.  
  15021. insertHTML() {
  15022. this.insertFavoritesPageHTML();
  15023. this.insertSearchPageHTML();
  15024. this.insertPostPageHTML();
  15025. }
  15026.  
  15027. insertFavoritesPageHTML() {
  15028. if (!Utils.onFavoritesPage()) {
  15029. return;
  15030. }
  15031. Utils.insertHTMLAndExtractStyle(document.getElementById("bottom-panel-4"), "beforeend", TagModifier.tagModifierHTML);
  15032. this.favoritesOption.container = document.getElementById("tag-modifier-container");
  15033. this.favoritesOption.checkbox = document.getElementById("tag-modifier-option-checkbox");
  15034. this.favoritesUI.container = document.getElementById("tag-modifier-ui-container");
  15035. this.favoritesUI.statusLabel = document.getElementById("tag-modifier-ui-status-label");
  15036. this.favoritesUI.textarea = document.getElementById("tag-modifier-ui-textarea");
  15037. this.favoritesUI.add = document.getElementById("tag-modifier-ui-add");
  15038. this.favoritesUI.remove = document.getElementById("tag-modifier-remove");
  15039. this.favoritesUI.reset = document.getElementById("tag-modifier-reset");
  15040. this.favoritesUI.selectAll = document.getElementById("tag-modifier-ui-select-all");
  15041. this.favoritesUI.unSelectAll = document.getElementById("tag-modifier-ui-un-select-all");
  15042. this.favoritesUI.import = document.getElementById("tag-modifier-import");
  15043. this.favoritesUI.export = document.getElementById("tag-modifier-export");
  15044. }
  15045.  
  15046. insertSearchPageHTML() {
  15047. if (!Utils.onSearchPage()) {
  15048. return;
  15049. }
  15050. 1;
  15051. }
  15052.  
  15053. insertPostPageHTML() {
  15054. if (!Utils.onPostPage()) {
  15055. return;
  15056. }
  15057. const contentContainer = document.querySelector(".flexi");
  15058. const originalAddToFavoritesLink = Array.from(document.querySelectorAll("a")).find(a => a.textContent === "Add to favorites");
  15059.  
  15060. const html = `
  15061. <div style="margin-bottom: 1em;">
  15062. <h4 class="image-sublinks">
  15063. <a href="#" id="add-to-favorites">Add to favorites</a>
  15064. |
  15065. <a href="#" id="add-custom-tags">Add custom tag</a>
  15066. <select id="custom-tags-list"></select>
  15067. </h4>
  15068. </div>
  15069. `;
  15070.  
  15071. if (contentContainer === null || originalAddToFavoritesLink === undefined) {
  15072. return;
  15073. }
  15074. contentContainer.insertAdjacentHTML("beforebegin", html);
  15075.  
  15076. const addToFavorites = document.getElementById("add-to-favorites");
  15077. const addCustomTags = document.getElementById("add-custom-tags");
  15078. const customTagsList = document.getElementById("custom-tags-list");
  15079.  
  15080. for (const customTag of Utils.customTags.values()) {
  15081. const option = document.createElement("option");
  15082.  
  15083. option.value = customTag;
  15084. option.textContent = customTag;
  15085. customTagsList.appendChild(option);
  15086. }
  15087. addToFavorites.onclick = () => {
  15088. originalAddToFavoritesLink.click();
  15089. return false;
  15090. };
  15091.  
  15092. addCustomTags.onclick = () => {
  15093. return false;
  15094. };
  15095. }
  15096.  
  15097. addEventListeners() {
  15098. this.addFavoritesPageEventListeners();
  15099. this.addSearchPageEventListeners();
  15100. this.addPostPageEventListeners();
  15101. }
  15102.  
  15103. addFavoritesPageEventListeners() {
  15104. if (!Utils.onFavoritesPage()) {
  15105. return;
  15106. }
  15107. this.favoritesOption.checkbox.onchange = (event) => {
  15108. this.toggleTagEditMode(event.target.checked);
  15109. };
  15110. this.favoritesUI.selectAll.onclick = this.selectAll.bind(this);
  15111. this.favoritesUI.unSelectAll.onclick = this.unSelectAll.bind(this);
  15112. this.favoritesUI.add.onclick = this.addTagsToSelected.bind(this);
  15113. this.favoritesUI.remove.onclick = this.removeTagsFromSelected.bind(this);
  15114. this.favoritesUI.reset.onclick = this.resetTagModifications.bind(this);
  15115. this.favoritesUI.import.onclick = this.importTagModifications.bind(this);
  15116. this.favoritesUI.export.onclick = this.exportTagModifications.bind(this);
  15117. window.addEventListener("searchStarted", () => {
  15118. this.unSelectAll();
  15119. });
  15120. window.addEventListener("changedPage", () => {
  15121. this.highlightSelectedThumbsOnPageChange();
  15122. });
  15123. }
  15124.  
  15125. addSearchPageEventListeners() {
  15126. if (!Utils.onSearchPage()) {
  15127. return;
  15128. }
  15129. 1;
  15130. }
  15131.  
  15132. addPostPageEventListeners() {
  15133. if (!Utils.onPostPage()) {
  15134. return;
  15135. }
  15136. 1;
  15137. }
  15138.  
  15139. highlightSelectedThumbsOnPageChange() {
  15140. if (!this.atLeastOneFavoriteIsSelected) {
  15141. return;
  15142. }
  15143. const posts = Utils.getAllThumbs()
  15144. .map(thumb => Post.allPosts.get(thumb.id));
  15145.  
  15146. for (const post of posts) {
  15147. if (post === undefined) {
  15148. return;
  15149. }
  15150.  
  15151. if (this.isSelectedForModification(post)) {
  15152. this.highlightPost(post, true);
  15153. }
  15154. }
  15155. }
  15156.  
  15157. /**
  15158. * @param {Boolean} value
  15159. */
  15160. toggleTagEditMode(value) {
  15161. this.toggleThumbInteraction(value);
  15162. this.toggleUI(value);
  15163. this.toggleTagEditModeEventListeners(value);
  15164. this.favoritesUI.unSelectAll.click();
  15165. }
  15166.  
  15167. /**
  15168. * @param {Boolean} value
  15169. */
  15170. toggleThumbInteraction(value) {
  15171. let html = "";
  15172.  
  15173. if (value) {
  15174. html =
  15175. `
  15176. .favorite {
  15177. cursor: pointer;
  15178. outline: 1px solid black;
  15179.  
  15180. > div,
  15181. >a
  15182. {
  15183. outline: none !important;
  15184.  
  15185. > img {
  15186. outline: none !important;
  15187. }
  15188.  
  15189. pointer-events:none;
  15190. opacity: 0.6;
  15191. filter: grayscale(40%);
  15192. transition: none !important;
  15193. }
  15194. }
  15195. `;
  15196. }
  15197. Utils.insertStyleHTML(html, "tag-edit-mode");
  15198. }
  15199.  
  15200. /**
  15201. * @param {Boolean} value
  15202. */
  15203. toggleUI(value) {
  15204. this.favoritesUI.container.style.display = value ? "block" : "none";
  15205. }
  15206.  
  15207. /**
  15208. * @param {Boolean} value
  15209. */
  15210. toggleTagEditModeEventListeners(value) {
  15211. if (!value) {
  15212. this.tagEditModeAbortController.abort();
  15213. this.tagEditModeAbortController = new AbortController();
  15214. return;
  15215. }
  15216.  
  15217. document.addEventListener("click", (event) => {
  15218. if (!event.target.classList.contains(Utils.favoriteItemClassName)) {
  15219. return;
  15220. }
  15221. const post = Post.allPosts.get(event.target.id);
  15222.  
  15223. if (post !== undefined) {
  15224. this.toggleThumbSelection(post);
  15225. }
  15226. }, {
  15227. signal: this.tagEditModeAbortController.signal
  15228. });
  15229.  
  15230. }
  15231.  
  15232. /**
  15233. * @param {String} text
  15234. */
  15235. showStatus(text) {
  15236. this.favoritesUI.statusLabel.style.visibility = "visible";
  15237. this.favoritesUI.statusLabel.textContent = text;
  15238. setTimeout(() => {
  15239. const statusHasNotChanged = this.favoritesUI.statusLabel.textContent === text;
  15240.  
  15241. if (statusHasNotChanged) {
  15242. this.favoritesUI.statusLabel.style.visibility = "hidden";
  15243. }
  15244. }, 1000);
  15245. }
  15246.  
  15247. unSelectAll() {
  15248. if (!this.atLeastOneFavoriteIsSelected) {
  15249. return;
  15250. }
  15251.  
  15252. for (const post of Post.allPosts.values()) {
  15253. this.toggleThumbSelection(post, false);
  15254. }
  15255. this.atLeastOneFavoriteIsSelected = false;
  15256. }
  15257.  
  15258. selectAll() {
  15259. for (const post of Post.postsMatchedBySearch.values()) {
  15260. this.toggleThumbSelection(post, true);
  15261. }
  15262. }
  15263.  
  15264. /**
  15265. * @param {Post} post
  15266. * @param {Boolean} value
  15267. */
  15268. toggleThumbSelection(post, value) {
  15269. this.atLeastOneFavoriteIsSelected = true;
  15270.  
  15271. if (value === undefined) {
  15272. value = !this.isSelectedForModification(post);
  15273. }
  15274. post.selectedForTagModification = value ? true : undefined;
  15275. this.highlightPost(post, value);
  15276. }
  15277.  
  15278. /**
  15279. * @param {Post} post
  15280. * @param {Boolean} value
  15281. */
  15282. highlightPost(post, value) {
  15283. if (post.root !== undefined) {
  15284. post.root.classList.toggle("tag-modifier-selected", value);
  15285. }
  15286. }
  15287.  
  15288. /**
  15289. * @param {Post} post
  15290. * @returns {Boolean}
  15291. */
  15292. isSelectedForModification(post) {
  15293. return post.selectedForTagModification !== undefined;
  15294. }
  15295.  
  15296. /**
  15297. * @param {String} tags
  15298. * @returns
  15299. */
  15300. removeContentTypeTags(tags) {
  15301. return tags
  15302. .replace(/(?:^|\s*)(?:video|animated|mp4)(?:$|\s*)/g, "");
  15303. }
  15304.  
  15305. addTagsToSelected() {
  15306. this.modifyTagsOfSelected(false);
  15307. }
  15308.  
  15309. removeTagsFromSelected() {
  15310. this.modifyTagsOfSelected(true);
  15311. }
  15312.  
  15313. /**
  15314. * @param {Boolean} remove
  15315. */
  15316. modifyTagsOfSelected(remove) {
  15317. const tags = this.favoritesUI.textarea.value.toLowerCase();
  15318. const tagsWithoutContentTypes = this.removeContentTypeTags(tags);
  15319. const tagsToModify = Utils.removeExtraWhiteSpace(tagsWithoutContentTypes);
  15320. const statusPrefix = remove ? "Removed tag(s) from" : "Added tag(s) to";
  15321. let modifiedTagsCount = 0;
  15322.  
  15323. if (tagsToModify === "") {
  15324. return;
  15325. }
  15326.  
  15327. for (const post of Post.allPosts.values()) {
  15328. if (this.isSelectedForModification(post)) {
  15329. const additionalTags = remove ? post.removeAdditionalTags(tagsToModify) : post.addAdditionalTags(tagsToModify);
  15330.  
  15331. TagModifier.tagModifications.set(post.id, additionalTags);
  15332. modifiedTagsCount += 1;
  15333. }
  15334. }
  15335.  
  15336. if (modifiedTagsCount === 0) {
  15337. return;
  15338. }
  15339.  
  15340. if (tags !== tagsWithoutContentTypes) {
  15341. alert("Warning: video, animated, and mp4 tags are unchanged.\nThey cannot be modified.");
  15342. }
  15343. this.showStatus(`${statusPrefix} ${modifiedTagsCount} favorite(s)`);
  15344. dispatchEvent(new Event("modifiedTags"));
  15345. Utils.setCustomTags(tagsToModify);
  15346. this.storeTagModifications();
  15347. }
  15348.  
  15349. createDatabase(event) {
  15350. /**
  15351. * @type {IDBDatabase}
  15352. */
  15353. const database = event.target.result;
  15354.  
  15355. database
  15356. .createObjectStore(TagModifier.objectStoreName, {
  15357. keyPath: "id"
  15358. });
  15359. }
  15360.  
  15361. storeTagModifications() {
  15362. const request = indexedDB.open(TagModifier.databaseName, 1);
  15363.  
  15364. request.onupgradeneeded = this.createDatabase;
  15365. request.onsuccess = (event) => {
  15366. /**
  15367. * @type {IDBDatabase}
  15368. */
  15369. const database = event.target.result;
  15370. const objectStore = database
  15371. .transaction(TagModifier.objectStoreName, "readwrite")
  15372. .objectStore(TagModifier.objectStoreName);
  15373. const idsWithNoTagModifications = [];
  15374.  
  15375. for (const [id, tags] of TagModifier.tagModifications) {
  15376. if (tags === "") {
  15377. idsWithNoTagModifications.push(id);
  15378. objectStore.delete(id);
  15379. } else {
  15380. objectStore.put({
  15381. id,
  15382. tags
  15383. });
  15384. }
  15385. }
  15386.  
  15387. for (const id of idsWithNoTagModifications) {
  15388. TagModifier.tagModifications.delete(id);
  15389. }
  15390. database.close();
  15391. };
  15392. }
  15393.  
  15394. loadTagModifications() {
  15395. const request = indexedDB.open(TagModifier.databaseName, 1);
  15396.  
  15397. request.onupgradeneeded = this.createDatabase;
  15398. request.onsuccess = (event) => {
  15399. /**
  15400. * @type {IDBDatabase}
  15401. */
  15402. const database = event.target.result;
  15403. const objectStore = database
  15404. .transaction(TagModifier.objectStoreName, "readonly")
  15405. .objectStore(TagModifier.objectStoreName);
  15406.  
  15407. objectStore.getAll().onsuccess = (successEvent) => {
  15408. const tagModifications = successEvent.target.result;
  15409.  
  15410. for (const record of tagModifications) {
  15411. TagModifier.tagModifications.set(record.id, record.tags);
  15412. }
  15413. this.restoreMissingCustomTags();
  15414. };
  15415. database.close();
  15416. };
  15417. }
  15418.  
  15419. restoreMissingCustomTags() {
  15420. // const allCustomTags = Array.from(TagModifier.tagModifications.values()).join(" ");
  15421. // const allUniqueCustomTags = new Set(allCustomTags.split(" "));
  15422.  
  15423. // Utils.setCustomTags(Array.from(allUniqueCustomTags).join(" "));
  15424. }
  15425.  
  15426. resetTagModifications() {
  15427. if (!confirm("Are you sure you want to delete all tag modifications?")) {
  15428. return;
  15429. }
  15430. Utils.customTags.clear();
  15431. indexedDB.deleteDatabase("AdditionalTags");
  15432. Post.allPosts.forEach(post => {
  15433. post.resetAdditionalTags();
  15434. });
  15435. dispatchEvent(new Event("modifiedTags"));
  15436. localStorage.removeItem("customTags");
  15437. }
  15438.  
  15439. exportTagModifications() {
  15440. const modifications = JSON.stringify(Utils.mapToObject(TagModifier.tagModifications));
  15441.  
  15442. navigator.clipboard.writeText(modifications);
  15443. alert("Copied tag modifications to clipboard");
  15444. }
  15445.  
  15446. importTagModifications() {
  15447. let modifications;
  15448.  
  15449. try {
  15450. const object = JSON.parse(this.favoritesUI.textarea.value);
  15451.  
  15452. if (!(typeof object === "object")) {
  15453. throw new TypeError(`Input parsed as ${typeof (object)}, but expected object`);
  15454. }
  15455. modifications = Utils.objectToMap(object);
  15456. } catch (error) {
  15457. if (error.name === "SyntaxError" || error.name === "TypeError") {
  15458. alert("Import Unsuccessful. Failed to parse input, JSON object format expected.");
  15459. } else {
  15460. throw error;
  15461. }
  15462. return;
  15463. }
  15464. console.error(modifications);
  15465. }
  15466. }
  15467.  
  15468. class AwesompleteImplementation {
  15469. static decodeEntities = (function() {
  15470. // this prevents any overhead from creating the object each time
  15471. const element = document.createElement("div");
  15472.  
  15473. function decodeHTMLEntities(str) {
  15474. if (str && typeof str === "string") {
  15475. // strip script/html tags
  15476. str = str.replace(/<script[^>]*>([\S\s]*?)<\/script>/gmi, "");
  15477. str = str.replace(/<\/?\w(?:[^"'>]|"[^"]*"|'[^']*')*>/gmi, "");
  15478. element.innerHTML = str;
  15479. str = element.textContent;
  15480. element.textContent = "";
  15481. }
  15482. return str;
  15483. }
  15484. return decodeHTMLEntities;
  15485. }());
  15486.  
  15487. static {
  15488. Utils.addStaticInitializer(() => {
  15489. // Awesomplete - Lea Verou - MIT license
  15490. !(function() {
  15491. function t(t) {
  15492. const e = Array.isArray(t) ? {
  15493. label: t[0],
  15494. value: t[1]
  15495. } : typeof t === "object" && t != null && "label" in t && "value" in t ? t : {
  15496. label: t,
  15497. value: t
  15498. };
  15499.  
  15500. this.label = e.label || e.value, this.value = e.value, this.type = e.type;
  15501. }
  15502.  
  15503. function e(t, e, i) {
  15504. for (const n in e) {
  15505. const s = e[n],
  15506. r = t.input.getAttribute(`data-${n.toLowerCase()}`);
  15507.  
  15508. 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);
  15509. }
  15510. }
  15511.  
  15512. function i(t, e) {
  15513. return typeof t === "string" ? (e || document).querySelector(t) : t || null;
  15514. }
  15515.  
  15516. function n(t, e) {
  15517. return o.call((e || document).querySelectorAll(t));
  15518. }
  15519.  
  15520. function s() {
  15521. n("input.awesomplete").forEach((t) => {
  15522. new r(t);
  15523. });
  15524. }
  15525.  
  15526. var r = function(t, n) {
  15527. const s = this;
  15528.  
  15529. this.isOpened = !1, this.input = i(t), this.input.setAttribute("autocomplete", "off"), this.input.setAttribute("aria-autocomplete", "list"), n = n || {}, e(this, {
  15530. minChars: 2,
  15531. maxItems: 20,
  15532. autoFirst: !1,
  15533. data: r.DATA,
  15534. filter: r.FILTER_CONTAINS,
  15535. sort: !1 !== n.sort && r.SORT_BYLENGTH,
  15536. item: r.ITEM,
  15537. replace: r.REPLACE
  15538. }, n), this.index = -1, this.container = i.create("div", {
  15539. className: "awesomplete",
  15540. around: t
  15541. }), this.ul = i.create("ul", {
  15542. hidden: "hidden",
  15543. inside: this.container
  15544. }), this.status = i.create("span", {
  15545. className: "visually-hidden",
  15546. role: "status",
  15547. "aria-live": "assertive",
  15548. "aria-relevant": "additions",
  15549. inside: this.container
  15550. }), this._events = {
  15551. input: {
  15552. input: this.evaluate.bind(this),
  15553. blur: this.close.bind(this, {
  15554. reason: "blur"
  15555. }),
  15556. keypress(t) {
  15557. const e = t.keyCode;
  15558.  
  15559. if (s.opened) {
  15560.  
  15561. switch (e) {
  15562. case 13: // RETURN
  15563. if (s.selected == true) {
  15564. t.preventDefault();
  15565. s.select();
  15566. break;
  15567. }
  15568.  
  15569. case 66:
  15570. break;
  15571.  
  15572. case 27: // ESC
  15573. s.close({
  15574. reason: "esc"
  15575. });
  15576. break;
  15577. }
  15578. }
  15579. },
  15580. keydown(t) {
  15581. const e = t.keyCode;
  15582.  
  15583. if (s.opened) {
  15584. switch (e) {
  15585. case 9: // TAB
  15586. if (s.selected == true) {
  15587. t.preventDefault();
  15588. s.select();
  15589. break;
  15590. }
  15591.  
  15592. case 38: // up arrow
  15593. t.preventDefault();
  15594. s.previous();
  15595. break;
  15596.  
  15597. case 40:
  15598. t.preventDefault();
  15599. s.next();
  15600. break;
  15601. }
  15602. }
  15603. }
  15604. },
  15605. form: {
  15606. submit: this.close.bind(this, {
  15607. reason: "submit"
  15608. })
  15609. },
  15610. ul: {
  15611. mousedown(t) {
  15612. let e = t.target;
  15613.  
  15614. if (e !== this) {
  15615. for (; e && !(/li/i).test(e.nodeName);) e = e.parentNode;
  15616. e && t.button === 0 && (t.preventDefault(), s.select(e, t.target));
  15617. }
  15618. }
  15619. }
  15620. }, 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);
  15621. };
  15622. r.prototype = {
  15623. set list(t) {
  15624. if (Array.isArray(t)) this._list = t;
  15625. else if (typeof t === "string" && t.indexOf(",") > -1) this._list = t.split(/\s*,\s*/);
  15626. else if ((t = i(t)) && t.children) {
  15627. const e = [];
  15628.  
  15629. o.apply(t.children).forEach((t) => {
  15630. if (!t.disabled) {
  15631. const i = t.textContent.trim(),
  15632. n = t.value || i,
  15633. s = t.label || i;
  15634.  
  15635. n !== "" && e.push({
  15636. label: s,
  15637. value: n
  15638. });
  15639. }
  15640. }), this._list = e;
  15641. }
  15642. document.activeElement === this.input && this.evaluate();
  15643. },
  15644. get selected() {
  15645. return this.index > -1;
  15646. },
  15647. get opened() {
  15648. return this.isOpened;
  15649. },
  15650. close(t) {
  15651. this.opened && (this.ul.setAttribute("hidden", ""), this.isOpened = !1, this.index = -1, i.fire(this.input, "awesomplete-close", t || {}));
  15652. },
  15653. open() {
  15654. this.ul.removeAttribute("hidden"), this.isOpened = !0, this.autoFirst && this.index === -1 && this.goto(0), i.fire(this.input, "awesomplete-open");
  15655. },
  15656. destroy() {
  15657. i.unbind(this.input, this._events.input), i.unbind(this.input.form, this._events.form);
  15658. const t = this.container.parentNode;
  15659.  
  15660. t.insertBefore(this.input, this.container), t.removeChild(this.container), this.input.removeAttribute("autocomplete"), this.input.removeAttribute("aria-autocomplete");
  15661. const e = r.all.indexOf(this);
  15662.  
  15663. e !== -1 && r.all.splice(e, 1);
  15664. },
  15665. next() {
  15666. const t = this.ul.children.length;
  15667.  
  15668. this.goto(this.index < t - 1 ? this.index + 1 : t ? 0 : -1);
  15669. },
  15670. previous() {
  15671. const t = this.ul.children.length,
  15672. e = this.index - 1;
  15673.  
  15674. this.goto(this.selected && e !== -1 ? e : t - 1);
  15675. },
  15676. goto(t) {
  15677. const e = this.ul.children;
  15678.  
  15679. 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", {
  15680. text: this.suggestions[this.index]
  15681. }));
  15682. },
  15683. select(t, e) {
  15684. if (t ? this.index = i.siblingIndex(t) : t = this.ul.children[this.index], t) {
  15685. const n = this.suggestions[this.index];
  15686.  
  15687. i.fire(this.input, "awesomplete-select", {
  15688. text: n,
  15689. origin: e || t
  15690. }) && (this.replace(n), this.close({
  15691. reason: "select"
  15692. }), i.fire(this.input, "awesomplete-selectcomplete", {
  15693. text: n
  15694. }));
  15695. }
  15696. },
  15697. evaluate() {
  15698. const e = this,
  15699. i = this.input.value;
  15700.  
  15701. i.length >= this.minChars && this._list.length > 0 ? (this.index = -1, this.ul.innerHTML = "", this.suggestions = this._list.map((n) => {
  15702. return new t(e.data(n, i));
  15703. }).filter((t) => {
  15704. return e.filter(t, i);
  15705. }), !1 !== this.sort && (this.suggestions = this.suggestions.sort(this.sort)), this.suggestions = this.suggestions.slice(0, this.maxItems), this.suggestions.forEach((t) => {
  15706. e.ul.appendChild(e.item(t, i));
  15707. }), this.ul.children.length === 0 ? this.close({
  15708. reason: "nomatches"
  15709. }) : this.open()) : this.close({
  15710. reason: "nomatches"
  15711. });
  15712. }
  15713. }, r.all = [], r.FILTER_CONTAINS = function(t, e) {
  15714. return RegExp(i.regExpEscape(e.trim()), "i").test(t);
  15715. }, r.FILTER_STARTSWITH = function(t, e) {
  15716. return RegExp(`^${i.regExpEscape(e.trim())}`, "i").test(t);
  15717. }, r.SORT_BYLENGTH = function(t, e) {
  15718. return t.length !== e.length ? t.length - e.length : t < e ? -1 : 1;
  15719. }, r.ITEM = function(t, e) {
  15720. return i.create("li", {
  15721. innerHTML: e.trim() === "" ? t : t.replace(RegExp(i.regExpEscape(e.trim()), "gi"), "<mark>$&</mark>"),
  15722. "aria-selected": "false"
  15723. });
  15724. }, r.REPLACE = function(t) {
  15725. this.input.value = t.value;
  15726. }, r.DATA = function(t) {
  15727. return t;
  15728. }, Object.defineProperty(t.prototype = Object.create(String.prototype), "length", {
  15729. get() {
  15730. return this.label.length;
  15731. }
  15732. }), t.prototype.toString = t.prototype.valueOf = function() {
  15733. return `${this.label}`;
  15734. };
  15735. var o = Array.prototype.slice;
  15736. i.create = function(t, e) {
  15737. const n = document.createElement(t);
  15738.  
  15739. for (const s in e) {
  15740. const r = e[s];
  15741.  
  15742. if (s === "inside") i(r).appendChild(n);
  15743. else if (s === "around") {
  15744. const o = i(r);
  15745.  
  15746. o.parentNode.insertBefore(n, o), n.appendChild(o);
  15747. } else s in n ? n[s] = r : n.setAttribute(s, r);
  15748. }
  15749. return n;
  15750. }, i.bind = function(t, e) {
  15751. if (t) for (const i in e) {
  15752. var n = e[i];
  15753. i.split(/\s+/).forEach((e) => {
  15754. t.addEventListener(e, n);
  15755. });
  15756. }
  15757. }, i.unbind = function(t, e) {
  15758. if (t) for (const i in e) {
  15759. var n = e[i];
  15760. i.split(/\s+/).forEach((e) => {
  15761. t.removeEventListener(e, n);
  15762. });
  15763. }
  15764. }, i.fire = function(t, e, i) {
  15765. const n = document.createEvent("HTMLEvents");
  15766.  
  15767. n.initEvent(e, !0, !0);
  15768.  
  15769. for (const s in i) n[s] = i[s];
  15770. return t.dispatchEvent(n);
  15771. }, i.regExpEscape = function(t) {
  15772. return t.replace(/[-\\^$*+?.()|[\]{}]/g, "\\$&");
  15773. }, i.siblingIndex = function(t) {
  15774. for (var e = 0; t = t.previousElementSibling; e++);
  15775. return e;
  15776. }, 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);
  15777. }());
  15778. });
  15779. }
  15780. }
  15781.  
  15782. class AwesompleteWrapper {
  15783. static preferences = {
  15784. savedSearchSuggestions: "savedSearchSuggestions"
  15785. };
  15786.  
  15787. /**
  15788. * @type {Boolean}
  15789. */
  15790. static get disabled() {
  15791. return !Utils.onFavoritesPage();
  15792. }
  15793.  
  15794. /**
  15795. * @type {Boolean}
  15796. */
  15797. showSavedSearchSuggestions;
  15798.  
  15799. constructor() {
  15800. if (AwesompleteWrapper.disabled) {
  15801. return;
  15802. }
  15803. this.initializeFields();
  15804. this.insertHTML();
  15805. this.addAwesompleteToInputs();
  15806. }
  15807.  
  15808. initializeFields() {
  15809. this.showSavedSearchSuggestions = Utils.getPreference(AwesompleteWrapper.preferences.savedSearchSuggestions, false);
  15810. }
  15811.  
  15812. insertHTML() {
  15813. Utils.createFavoritesOption(
  15814. "show-saved-search-suggestions",
  15815. "Saved Suggestions",
  15816. "Show saved search suggestions in autocomplete dropdown",
  15817. this.showSavedSearchSuggestions,
  15818. (event) => {
  15819. this.showSavedSearchSuggestions = event.target.checked;
  15820. Utils.setPreference(AwesompleteWrapper.preferences.savedSearchSuggestions, event.target.checked);
  15821. },
  15822. false
  15823. );
  15824. }
  15825.  
  15826. addAwesompleteToInputs() {
  15827. document.querySelectorAll("textarea").forEach((textarea) => {
  15828. this.addAwesompleteToInput(textarea);
  15829. });
  15830. document.querySelectorAll("input").forEach((input) => {
  15831. if (input.hasAttribute("needs-autocomplete")) {
  15832. this.addAwesompleteToInput(input);
  15833. }
  15834. });
  15835. }
  15836.  
  15837. /**
  15838. * @param {HTMLElement} input
  15839. */
  15840. addAwesompleteToInput(input) {
  15841. const awesomplete = new Awesomplete_(input, {
  15842. minChars: 1,
  15843. list: [],
  15844. filter: (suggestion, _) => {
  15845. // eslint-disable-next-line new-cap
  15846. return Awesomplete_.FILTER_STARTSWITH(suggestion.value, this.getCurrentTag(awesomplete.input));
  15847. },
  15848. sort: false,
  15849. item: (suggestion, tags) => {
  15850. const html = tags.trim() === "" ? suggestion.label : suggestion.label.replace(RegExp(Awesomplete_.$.regExpEscape(tags.trim()), "gi"), "<mark>$&</mark>");
  15851. return Awesomplete_.$.create("li", {
  15852. innerHTML: html,
  15853. "aria-selected": "false",
  15854. className: `tag-type-${suggestion.type}`
  15855. });
  15856. },
  15857. replace: (suggestion) => {
  15858. Utils.insertSuggestion(awesomplete.input, Utils.removeSavedSearchPrefix(decodeEntities(suggestion.value)));
  15859. }
  15860. });
  15861.  
  15862. input.addEventListener("keydown", (event) => {
  15863. switch (event.key) {
  15864. case "Tab":
  15865. if (!awesomplete.isOpened || awesomplete.suggestions.length === 0) {
  15866. return;
  15867. }
  15868. awesomplete.next();
  15869. awesomplete.select();
  15870. event.preventDefault();
  15871. break;
  15872.  
  15873. case "Escape":
  15874. Utils.hideAwesomplete(input);
  15875. break;
  15876.  
  15877. default:
  15878. break;
  15879. }
  15880. });
  15881.  
  15882. input.oninput = () => {
  15883. this.populateAwesompleteList(input.id, this.getCurrentTagWithHyphen(input), awesomplete);
  15884. };
  15885. }
  15886.  
  15887. getSavedSearchesForAutocompleteList(inputId, prefix) {
  15888. if (Utils.onMobileDevice() || !this.showSavedSearchSuggestions || inputId !== "favorites-search-box") {
  15889. return [];
  15890. }
  15891. return Utils.getSavedSearchesForAutocompleteList(prefix);
  15892. }
  15893.  
  15894. /**
  15895. * @param {String} inputId
  15896. * @param {String} prefix
  15897. * @param {Awesomplete_} awesomplete
  15898. */
  15899. populateAwesompleteList(inputId, prefix, awesomplete) {
  15900. if (prefix.trim() === "") {
  15901. return;
  15902. }
  15903. const savedSearchSuggestions = this.getSavedSearchesForAutocompleteList(inputId, prefix);
  15904.  
  15905. prefix = prefix.replace(/^-/, "");
  15906.  
  15907. fetch(`https://ac.rule34.xxx/autocomplete.php?q=${prefix}`)
  15908. .then((response) => {
  15909. if (response.ok) {
  15910. return response.text();
  15911. }
  15912. throw new Error(response.status);
  15913. })
  15914. .then((suggestions) => {
  15915.  
  15916. const mergedSuggestions = Utils.addCustomTagsToAutocompleteList(JSON.parse(suggestions), prefix);
  15917.  
  15918. awesomplete.list = mergedSuggestions.concat(savedSearchSuggestions);
  15919. });
  15920. }
  15921.  
  15922. /**
  15923. * @param {HTMLInputElement | HTMLTextAreaElement} input
  15924. * @returns {String}
  15925. */
  15926. getCurrentTag(input) {
  15927. return this.getLastTag(input.value.slice(0, input.selectionStart));
  15928. }
  15929.  
  15930. /**
  15931. * @param {String} searchQuery
  15932. * @returns {String}
  15933. */
  15934. getLastTag(searchQuery) {
  15935. const lastTag = searchQuery.match(/[^ -][^ ]*$/);
  15936. return lastTag === null ? "" : lastTag[0];
  15937. }
  15938.  
  15939. /**
  15940. * @param {HTMLInputElement | HTMLTextAreaElement} input
  15941. * @returns {String}
  15942. */
  15943. getCurrentTagWithHyphen(input) {
  15944. return this.getLastTagWithHyphen(input.value.slice(0, input.selectionStart));
  15945. }
  15946.  
  15947. /**
  15948. * @param {String} searchQuery
  15949. * @returns {String}
  15950. */
  15951. getLastTagWithHyphen(searchQuery) {
  15952. const lastTag = searchQuery.match(/[^ ]*$/);
  15953. return lastTag === null ? "" : lastTag[0];
  15954. }
  15955. }
  15956.  
  15957. Utils.initialize();
  15958. const favoritesLoader = new FavoritesLoader();
  15959. const favoritesMenu = new FavoritesMenu();
  15960. const gallery = new Gallery();
  15961. const tooltip = new Tooltip();
  15962. const savedSearches = new SavedSearches();
  15963. const caption = new Caption();
  15964. const tagModifier = new TagModifier();
  15965. const awesompleteWrapper = new AwesompleteWrapper();
  15966.  
  15967. Utils.postProcess();