// ==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);