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-08-05 提交的版本,查看 最新版本

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

QingJ © 2025

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