Iwara Custom Sort

Automatically sort teaser images on /videos, /images, /subscriptions, /users, and sidebars using customizable sort function.

当前为 2019-02-16 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Iwara Custom Sort
  3. // @version 0.153
  4. // @grant GM.setValue
  5. // @grant GM.getValue
  6. // @grant GM.deleteValue
  7. // @run-at document-start
  8. // @noframes
  9. // @match https://ecchi.iwara.tv/*
  10. // @match https://www.iwara.tv/*
  11. // @match http://ecchi.iwara.tv/*
  12. // @match http://www.iwara.tv/*
  13. // @description Automatically sort teaser images on /videos, /images, /subscriptions, /users, and sidebars using customizable sort function.
  14. // @license AGPL-3.0-or-later
  15. // @namespace https://gf.qytechs.cn/users/245195
  16. // ==/UserScript==
  17.  
  18. /* jshint esversion: 6 */
  19. /* global GM */
  20.  
  21. 'use strict';
  22.  
  23. const additionalPageCount = 0;
  24.  
  25. const logDebug = (...args) => {
  26. const debugging = true;
  27. if (debugging) {
  28. console.log(...args);
  29. }
  30. };
  31.  
  32. const teaserDivSelector = '.node-teaser';
  33.  
  34. const getTeaserGrids = (node) => {
  35. const teaserGridSelector = '.views-responsive-grid';
  36. return Array.from(node.querySelectorAll(teaserGridSelector))
  37. .filter(grid => grid.querySelector(teaserDivSelector));
  38. };
  39.  
  40. const timeout = delay => new Promise(resolve => setTimeout(resolve, delay));
  41.  
  42. const sortTeasers = (grid, valueExpression) => {
  43. const viewsIconSelector = '.glyphicon-eye-open';
  44. const likesIconSelector = '.glyphicon-heart';
  45. const imageFieldSelector = '.field-type-image';
  46. const galleryIconSelector = '.glyphicon-th-large';
  47. const privateDivSelector = '.private-video';
  48. const teaserDivs = Array.from(grid.querySelectorAll(teaserDivSelector));
  49. const getNearbyNumber = (element) => {
  50. const parsePrefixed = str => Number.parseFloat(str) * (str.includes('k') ? 1000 : 1);
  51. return element ? parsePrefixed(element.parentElement.textContent) : 0;
  52. };
  53. const teaserItems = teaserDivs.map(div => ({
  54. div,
  55. viewCount: getNearbyNumber(div.querySelector(viewsIconSelector)),
  56. likeCount: getNearbyNumber(div.querySelector(likesIconSelector)),
  57. imageFactor: div.querySelector(imageFieldSelector) ? 1 : 0,
  58. galleryFactor: div.querySelector(galleryIconSelector) ? 1 : 0,
  59. privateFactor: div.querySelector(privateDivSelector) ? 1 : 0,
  60. }));
  61. const evalSortValue = (item, expression) =>
  62. // eslint-disable-next-line no-new-func
  63. new Function(
  64. 'views',
  65. 'likes',
  66. 'ratio',
  67. 'image',
  68. 'gallery',
  69. 'private',
  70. `return (${expression})`,
  71. )(
  72. item.viewCount,
  73. item.likeCount,
  74. Math.min(item.likeCount / Math.max(1, item.viewCount), 1),
  75. item.imageFactor,
  76. item.galleryFactor,
  77. item.privateFactor,
  78. );
  79. teaserItems.forEach((item) => {
  80. // eslint-disable-next-line no-param-reassign
  81. item.sortValue = evalSortValue(item, valueExpression);
  82. });
  83. teaserItems.sort((itemA, itemB) => itemB.sortValue - itemA.sortValue);
  84. teaserDivs.map((div) => {
  85. const anchor = document.createElement('div');
  86. div.before(anchor);
  87. return anchor;
  88. }).forEach((div, index) => div.replaceWith(teaserItems[index].div));
  89. };
  90.  
  91. const sortAllTeasers = (valueExpression) => {
  92. GM.setValue('sortValue', valueExpression);
  93. let sortedCount = 0;
  94. try {
  95. getTeaserGrids(document).forEach((grid) => {
  96. sortTeasers(grid, valueExpression);
  97. sortedCount += 1;
  98. });
  99. } catch (message) {
  100. alert(message);
  101. }
  102. logDebug(`${sortedCount} grids sorted at ${window.location}`);
  103. };
  104.  
  105. const getNumberParam = (URL, name) => {
  106. const params = URL.searchParams;
  107. return params.has(name) ? Number.parseInt(params.get(name)) : 0;
  108. };
  109.  
  110. const getPageParam = URL => getNumberParam(URL, 'page');
  111.  
  112. const addAdditionalPages = (URL) => {
  113. const params = URL.searchParams;
  114. let page = getPageParam(URL);
  115. for (let pageLeft = additionalPageCount; pageLeft > 0; pageLeft -= 1) {
  116. page += 1;
  117. params.set('page', page);
  118. const nextPage = document.createElement('embed');
  119. nextPage.src = URL;
  120. nextPage.style.display = 'none';
  121. logDebug('page', nextPage.src, pageLeft);
  122. document.documentElement.append(nextPage);
  123. }
  124. };
  125.  
  126. const createTextInput = (text, maxLength, size) => {
  127. const input = document.createElement('input');
  128. input.value = text;
  129. input.maxLength = maxLength;
  130. input.size = size;
  131. return input;
  132. };
  133.  
  134. const createButton = (text, clickHandler) => {
  135. const button = document.createElement('button');
  136. button.innerHTML = text;
  137. button.addEventListener('click', clickHandler);
  138. return button;
  139. };
  140.  
  141. const createUI = async () => {
  142. const defaultValue = '(ratio / (private * 2.5 + 1) + Math.sqrt(likes) / 3000) / (image + 3)';
  143. const sortValueInput = createTextInput(await GM.getValue('sortValue', defaultValue), 120, 60);
  144. const sortButton = createButton('Sort', () => sortAllTeasers(sortValueInput.value));
  145. sortValueInput.addEventListener('keyup', (event) => {
  146. if (event.key === 'Enter') {
  147. sortButton.click();
  148. }
  149. });
  150. const resetDefaultButton = createButton('Default', () => {
  151. sortValueInput.value = defaultValue;
  152. });
  153. return {
  154. sortValueInput,
  155. sortButton,
  156. resetDefaultButton,
  157. };
  158. };
  159.  
  160. const addUI = (UI) => {
  161. const UIDiv = document.createElement('div');
  162. UIDiv.style.display = 'inline';
  163. UIDiv.style.margin = '5px';
  164. UIDiv.append(UI.resetDefaultButton, UI.sortValueInput, UI.sortButton);
  165. document.querySelector('#user-links').prepend(UIDiv);
  166. };
  167.  
  168. const addTeasersToParent = (teaserGrids) => {
  169. const parentGrids = getTeaserGrids(window.parent.document);
  170. for (let i = 0, j = 0; i < parentGrids.length; i += 1) {
  171. if (teaserGrids[j].className === parentGrids[i].className) {
  172. // eslint-disable-next-line no-param-reassign
  173. teaserGrids[j].className = '';
  174. parentGrids[i].prepend(teaserGrids[j]);
  175. j += 1;
  176. }
  177. }
  178. };
  179.  
  180. const adjustPageAnchors = (container) => {
  181. const currentPage = getPageParam(new URL(window.location));
  182. const changePageParam = (anchor, value) => {
  183. const anchorURL = new URL(anchor.href, window.location);
  184. anchorURL.searchParams.set('page', value);
  185. // eslint-disable-next-line no-param-reassign
  186. anchor.href = anchorURL.pathname + anchorURL.search;
  187. };
  188. if (currentPage > 0) {
  189. const previousPageAnchor = container.querySelector('.pager-previous a');
  190. changePageParam(previousPageAnchor, Math.max(0, currentPage - 1 - additionalPageCount));
  191. }
  192. const nextPage = currentPage + 1 + additionalPageCount;
  193. {
  194. const lastPageAnchor = container.querySelector('.pager-last a');
  195. if (lastPageAnchor) {
  196. const nextPageAnchor = container.querySelector('.pager-next a');
  197. if (getPageParam(new URL(lastPageAnchor.href, window.location)) >= nextPage) {
  198. changePageParam(nextPageAnchor, nextPage);
  199. } else {
  200. nextPageAnchor.remove();
  201. lastPageAnchor.remove();
  202. }
  203. }
  204. }
  205. const loadedPageAnchors = Array.from(container.querySelectorAll('.pager-item a'))
  206. .filter((anchor) => {
  207. const page = getPageParam(new URL(anchor.href, window.location));
  208. return page >= currentPage && page < nextPage;
  209. });
  210. if (loadedPageAnchors.length > 0) {
  211. const parentItem = document.createElement('li');
  212. const groupList = document.createElement('ul');
  213. groupList.style.display = 'inline';
  214. groupList.style.backgroundColor = 'hsla(0, 0%, 75%, 50%)';
  215. loadedPageAnchors[0].parentElement.before(parentItem);
  216. const currentPageItem = container.querySelector('.pager-current');
  217. currentPageItem.style.marginLeft = '0';
  218. groupList.append(currentPageItem);
  219. loadedPageAnchors.forEach((anchor) => {
  220. anchor.parentNode.classList.toggle('pager-item', false);
  221. anchor.parentNode.classList.toggle('pager-current', true);
  222. groupList.append(anchor.parentElement);
  223. });
  224. parentItem.append(groupList);
  225. }
  226. };
  227.  
  228. const adjustAnchors = () => {
  229. const pageAnchorList = document.querySelectorAll('.pager');
  230. pageAnchorList.forEach((list) => {
  231. adjustPageAnchors(list);
  232. });
  233. };
  234.  
  235. const initParent = async (teasersAddedMeesage) => {
  236. const UI = await createUI();
  237. addUI(UI);
  238. window.addEventListener('message', (event) => {
  239. if (
  240. new URL(event.origin).hostname === window.location.hostname &&
  241. event.data === teasersAddedMeesage
  242. ) {
  243. sortAllTeasers(UI.sortValueInput.value);
  244. }
  245. });
  246. UI.sortButton.click();
  247. const extraPageRegEx = /\/(videos|images|subscriptions)$/;
  248. if (extraPageRegEx.test(window.location.pathname)) {
  249. addAdditionalPages(new URL(window.location));
  250. adjustAnchors();
  251. }
  252. };
  253.  
  254. const init = async () => {
  255. const teaserGrids = getTeaserGrids(document);
  256. if (teaserGrids.length === 0) {
  257. return;
  258. }
  259. const teasersAddedMeesage = 'iwara custom sort: teasersAdded';
  260. if (window === window.parent) {
  261. logDebug('I am a Parent.');
  262. initParent(teasersAddedMeesage);
  263. } else {
  264. logDebug('I am a child.');
  265. await timeout(500);
  266. addTeasersToParent(teaserGrids);
  267. window.parent.postMessage(teasersAddedMeesage, window.location.origin);
  268. }
  269. };
  270.  
  271. logDebug(`Parsed:${window.location}, ${document.readyState} Parent:`, window.parent);
  272. document.addEventListener('DOMContentLoaded', init);

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址