Iwara Custom Sort

Automatically sort teaser images on /videos, /images, /subscriptions, /users, and sidebars using customizable sort function. Can load and sort multiple pages at once.

当前为 2019-06-20 提交的版本,查看 最新版本

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

QingJ © 2025

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