// ==UserScript==
// @name Iwara Custom Sort
// @version 0.186
// @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. Can load and sort multiple pages at once.
// @license AGPL-3.0-or-later
// @namespace https://gf.qytechs.cn/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 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 = document.createElement('embed');
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, 10, 1, '3em');
pageCountInput.addEventListener('change', (event) => {
GM.setValue('pageCount', Number.parseInt(event.target.value));
});
const lable2 = createSpan('pages loaded.', 'white');
const defaultValue = '(ratio / (private * 2.5 + 1) + Math.sqrt(likes) / 3000) / (image + 3)';
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 = '';
parentGrids[i].prepend(teaserGrids[j]);
j += 1;
}
}
};
const adjustPageAnchors = (container, pageCount) => {
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 - 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);
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)) - getPageParam(new URL(window.location)) - 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);