Iwara Custom Sort

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

16.02.2019 itibariyledir. En son verisyonu görün.

// ==UserScript==
// @name     Iwara Custom Sort
// @version  0.153
// @grant    GM.setValue
// @grant    GM.getValue
// @grant    GM.deleteValue
// @run-at   document-start
// @noframes
// @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.
// @license  AGPL-3.0-or-later
// @namespace https://greasyfork.org/users/245195
// ==/UserScript==

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

'use strict';

const additionalPageCount = 0;

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

const teaserDivSelector = '.node-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 at ${window.location}`);
};

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 addAdditionalPages = (URL) => {
  const params = URL.searchParams;
  let page = getPageParam(URL);
  for (let pageLeft = additionalPageCount; pageLeft > 0; pageLeft -= 1) {
    page += 1;
    params.set('page', page);
    const nextPage = document.createElement('embed');
    nextPage.src = URL;
    nextPage.style.display = 'none';
    logDebug('page', nextPage.src, pageLeft);
    document.documentElement.append(nextPage);
  }
};

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 createUI = async () => {
  const defaultValue = '(ratio / (private * 2.5 + 1) + Math.sqrt(likes) / 3000) / (image + 3)';
  const sortValueInput = createTextInput(await GM.getValue('sortValue', defaultValue), 120, 60);
  const sortButton = createButton('Sort', () => sortAllTeasers(sortValueInput.value));
  sortValueInput.addEventListener('keyup', (event) => {
    if (event.key === 'Enter') {
      sortButton.click();
    }
  });
  const resetDefaultButton = createButton('Default', () => {
    sortValueInput.value = defaultValue;
  });
  return {
    sortValueInput,
    sortButton,
    resetDefaultButton,
  };
};

const addUI = (UI) => {
  const UIDiv = document.createElement('div');
  UIDiv.style.display = 'inline';
  UIDiv.style.margin = '5px';
  UIDiv.append(UI.resetDefaultButton, UI.sortValueInput, UI.sortButton);
  document.querySelector('#user-links').prepend(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 = '';
      parentGrids[i].prepend(teaserGrids[j]);
      j += 1;
    }
  }
};

const adjustPageAnchors = (container) => {
  const currentPage = getPageParam(new URL(window.location));
  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 - 1 - additionalPageCount));
  }
  const nextPage = currentPage + 1 + additionalPageCount;
  {
    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.toggle('pager-item', false);
      anchor.parentNode.classList.toggle('pager-current', true);
      groupList.append(anchor.parentElement);
    });
    parentItem.append(groupList);
  }
};

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

const initParent = async (teasersAddedMeesage) => {
  const UI = await createUI();
  addUI(UI);
  window.addEventListener('message', (event) => {
    if (
      new URL(event.origin).hostname === window.location.hostname &&
      event.data === teasersAddedMeesage
    ) {
      sortAllTeasers(UI.sortValueInput.value);
    }
  });
  UI.sortButton.click();
  const extraPageRegEx = /\/(videos|images|subscriptions)$/;
  if (extraPageRegEx.test(window.location.pathname)) {
    addAdditionalPages(new URL(window.location));
    adjustAnchors();
  }
};

const init = async () => {
  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.');
    await timeout(500);
    addTeasersToParent(teaserGrids);
    window.parent.postMessage(teasersAddedMeesage, window.location.origin);
  }
};

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