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-05-29 提交的版本,檢視 最新版本

// ==UserScript==
// @name     Iwara Custom Sort
// @version  0.192
// @grant    GM.setValue
// @grant    GM.getValue
// @grant    GM.deleteValue
// @run-at   document-start
// @match    https://ecchi.iwara.tv/*
// @match    https://www.iwara.tv/*
// @match    http://ecchi.iwara.tv/*
// @match    http://www.iwara.tv/*
// @description  Automatically sort teaser images on /videos, /images, /subscriptions, /users, and sidebars using customizable sort function. Can load and sort multiple pages at once.
// @license  AGPL-3.0-or-later
// @namespace https://greasyfork.org/users/245195
// ==/UserScript==

/* jshint esversion: 6 */
/* global GM */

'use strict';

const logDebug = (...args) => {
  const debugging = true;
  if (debugging) {
    console.log(...args);
  }
};

const teaserDivSelector = '.node-teaser, .node-sidebar_teaser';

const getTeaserGrids = (node) => {
  const teaserGridSelector = '.views-responsive-grid';
  return Array.from(node.querySelectorAll(teaserGridSelector))
    .filter(grid => grid.querySelector(teaserDivSelector));
};

const timeout = delay => new Promise(resolve => setTimeout(resolve, delay));

const sortTeasers = (grid, valueExpression) => {
  const viewsIconSelector = '.glyphicon-eye-open';
  const likesIconSelector = '.glyphicon-heart';
  const imageFieldSelector = '.field-type-image';
  const galleryIconSelector = '.glyphicon-th-large';
  const privateDivSelector = '.private-video';
  const teaserDivs = Array.from(grid.querySelectorAll(teaserDivSelector));
  const getNearbyNumber = (element) => {
    const parsePrefixed = str => Number.parseFloat(str) * (str.includes('k') ? 1000 : 1);
    return element ? parsePrefixed(element.parentElement.textContent) : 0;
  };
  const teaserItems = teaserDivs.map(div => ({
    div,
    viewCount: getNearbyNumber(div.querySelector(viewsIconSelector)),
    likeCount: getNearbyNumber(div.querySelector(likesIconSelector)),
    imageFactor: div.querySelector(imageFieldSelector) ? 1 : 0,
    galleryFactor: div.querySelector(galleryIconSelector) ? 1 : 0,
    privateFactor: div.querySelector(privateDivSelector) ? 1 : 0,
  }));
  const evalSortValue = (item, expression) =>
    // eslint-disable-next-line no-new-func
    new Function(
      'views',
      'likes',
      'ratio',
      'image',
      'gallery',
      'private',
      `return (${expression})`,
    )(
      item.viewCount,
      item.likeCount,
      Math.min(item.likeCount / Math.max(1, item.viewCount), 1),
      item.imageFactor,
      item.galleryFactor,
      item.privateFactor,
    );
  teaserItems.forEach((item) => {
    // eslint-disable-next-line no-param-reassign
    item.sortValue = evalSortValue(item, valueExpression);
  });
  teaserItems.sort((itemA, itemB) => itemB.sortValue - itemA.sortValue);
  teaserDivs.map((div) => {
    const anchor = document.createElement('div');
    div.before(anchor);
    return anchor;
  }).forEach((div, index) => div.replaceWith(teaserItems[index].div));
};

const sortAllTeasers = (valueExpression) => {
  GM.setValue('sortValue', valueExpression);
  let sortedCount = 0;
  try {
    getTeaserGrids(document).forEach((grid) => {
      sortTeasers(grid, valueExpression);
      sortedCount += 1;
    });
  } catch (message) {
    alert(message);
  }
  logDebug(`${sortedCount} grids sorted`);
};

const getNumberParam = (URL, name) => {
  const params = URL.searchParams;
  return params.has(name) ? Number.parseInt(params.get(name)) : 0;
};

const getPageParam = URL => getNumberParam(URL, 'page');
const currentPage = getPageParam(new URL(window.location));

const createAdditionalPages = (URL, additionalPageCount) => {
  const params = URL.searchParams;
  let page = getPageParam(URL);
  const pages = [];
  for (let pageLeft = additionalPageCount; pageLeft > 0; pageLeft -= 1) {
    page += 1;
    params.set('page', page);
    const nextPage = (() =>
      (navigator.userAgent.indexOf('Firefox') > -1
        ? document.createElement('embed')
        : document.createElement('iframe'))
    )();
    nextPage.src = URL;
    nextPage.style.display = 'none';
    logDebug('Add page:', nextPage.src);
    pages.push(nextPage);
  }
  return pages;
};

const createTextInput = (text, maxLength, size) => {
  const input = document.createElement('input');
  input.value = text;
  input.maxLength = maxLength;
  input.size = size;
  return input;
};

const createButton = (text, clickHandler) => {
  const button = document.createElement('button');
  button.innerHTML = text;
  button.addEventListener('click', clickHandler);
  return button;
};

const createNumberInput = (value, min, max, step, width) => {
  const input = document.createElement('input');
  Object.assign(input, {
    type: 'number', value, min, max, step,
  });
  input.setAttribute('required', '');
  input.style.width = width;
  return input;
};

const createSpan = (text, color) => {
  const span = document.createElement('span');
  span.innerHTML = text;
  span.style.color = color;
  return span;
};

const createUI = async (pageCount) => {
  const lable1 = createSpan('1 of', 'white');
  const pageCountInput = createNumberInput(pageCount, 1, 15, 1, '3em');
  const lable2 = createSpan('pages loaded.', 'white');
  pageCountInput.addEventListener('change', (event) => {
    GM.setValue('pageCount', Number.parseInt(event.target.value));
    lable2.innerHTML = 'pages loaded. Refresh to apply the change';
  });
  const defaultValue = '(ratio / (private * 2.0 + 1) + Math.log(likes) / 250) / (image + 8.0)';
  const sortValueInput = createTextInput(await GM.getValue('sortValue', defaultValue), 120, 60);
  sortValueInput.classList.add('form-text');
  const sortButton = createButton('Sort', () => sortAllTeasers(sortValueInput.value));
  sortButton.classList.add('btn', 'btn-sm', 'btn-primary');
  sortValueInput.addEventListener('keyup', (event) => {
    if (event.key === 'Enter') {
      sortButton.click();
    }
  });
  const resetDefaultButton = createButton('Default', () => {
    sortValueInput.value = defaultValue;
  });
  resetDefaultButton.classList.add('btn', 'btn-sm', 'btn-info');
  return {
    lable1,
    pageCountInput,
    lable2,
    sortValueInput,
    sortButton,
    resetDefaultButton,
  };
};

const addUI = (UI) => {
  const UIDiv = document.createElement('div');
  UIDiv.style.display = 'inline-block';
  UIDiv.append(
    UI.sortValueInput,
    UI.resetDefaultButton,
    UI.sortButton,
    UI.lable1,
    UI.pageCountInput,
    UI.lable2,
  );
  UIDiv.childNodes.forEach((node) => {
    // eslint-disable-next-line no-param-reassign
    node.style.margin = '5px 2px';
  });
  document.querySelector('#user-links').after(UIDiv);
};

const addTeasersToParent = (teaserGrids) => {
  const parentGrids = getTeaserGrids(window.parent.document);
  for (let i = 0, j = 0; i < parentGrids.length; i += 1) {
    if (teaserGrids[j].className === parentGrids[i].className) {
      // eslint-disable-next-line no-param-reassign
      teaserGrids[j].className = '';
      teaserGrids[j].setAttribute('originPage', currentPage);
      parentGrids[i].prepend(teaserGrids[j]);
      j += 1;
    }
  }
};

const adjustPageAnchors = (container, pageCount) => {
  const changePageParam = (anchor, value) => {
    const anchorURL = new URL(anchor.href, window.location);
    anchorURL.searchParams.set('page', value);
    // eslint-disable-next-line no-param-reassign
    anchor.href = anchorURL.pathname + anchorURL.search;
  };
  if (currentPage > 0) {
    const previousPageAnchor = container.querySelector('.pager-previous a');
    changePageParam(previousPageAnchor, Math.max(0, currentPage - pageCount));
  }
  const nextPage = currentPage + pageCount;
  {
    const lastPageAnchor = container.querySelector('.pager-last a');
    if (lastPageAnchor) {
      const nextPageAnchor = container.querySelector('.pager-next a');
      if (getPageParam(new URL(lastPageAnchor.href, window.location)) >= nextPage) {
        changePageParam(nextPageAnchor, nextPage);
      } else {
        nextPageAnchor.remove();
        lastPageAnchor.remove();
      }
    }
  }
  const loadedPageAnchors = Array.from(container.querySelectorAll('.pager-item a'))
    .filter((anchor) => {
      const page = getPageParam(new URL(anchor.href, window.location));
      return page >= currentPage && page < nextPage;
    });
  if (loadedPageAnchors.length > 0) {
    const parentItem = document.createElement('li');
    const groupList = document.createElement('ul');
    groupList.style.display = 'inline';
    groupList.style.backgroundColor = 'hsla(0, 0%, 75%, 50%)';
    loadedPageAnchors[0].parentElement.before(parentItem);
    const currentPageItem = container.querySelector('.pager-current');
    currentPageItem.style.marginLeft = '0';
    groupList.append(currentPageItem);
    loadedPageAnchors.forEach((anchor) => {
      anchor.parentNode.classList.remove('pager-item');
      anchor.parentNode.classList.add('pager-current');
      groupList.append(anchor.parentElement);
    });
    parentItem.append(groupList);
  }
};

const adjustAnchors = (pageCount) => {
  const pageAnchorList = document.querySelectorAll('.pager');
  pageAnchorList.forEach((list) => {
    adjustPageAnchors(list, pageCount);
  });
};

const initParent = async (teasersAddedMeesage) => {
  const pageCount = await GM.getValue('pageCount', 1);
  const UI = await createUI(pageCount);
  addUI(UI);
  const extraPageRegEx = /\/(videos|images|subscriptions)$/;
  let pages = [];
  if (extraPageRegEx.test(window.location.pathname)) {
    pages = createAdditionalPages(new URL(window.location), pageCount - 1);
    document.body.append(...pages);
    logDebug(pages);
    adjustAnchors(pageCount);
  }
  let loadedPageCount = 1;
  window.addEventListener('message', (event) => {
    if (
      new URL(event.origin).hostname === window.location.hostname
      && event.data === teasersAddedMeesage
    ) {
      sortAllTeasers(UI.sortValueInput.value);
      const loadedPage = pages[
        getPageParam(new URL(event.source.location)) - currentPage - 1
      ];
      loadedPage.src = '';
      loadedPage.remove();
      loadedPageCount += 1;
      UI.lable1.innerHTML = `${loadedPageCount} of `;
    }
  });
  UI.sortButton.click();
};

const init = async () => {
  try {
    const teaserGrids = getTeaserGrids(document);
    if (teaserGrids.length === 0) {
      return;
    }
    const teasersAddedMeesage = 'iwara custom sort: teasersAdded';
    if (window === window.parent) {
      logDebug('I am a Parent.');
      initParent(teasersAddedMeesage);
    } else {
      logDebug('I am a child.', window.location, window.parent.location);
      await timeout(500);
      addTeasersToParent(teaserGrids);
      window.parent.postMessage(teasersAddedMeesage, window.location.origin);
    }
  } catch (error) {
    logDebug(error);
  }
};

logDebug(`Parsed:${window.location}, ${document.readyState} Parent:`, window.parent);
document.addEventListener('DOMContentLoaded', init);