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.

Ajankohdalta 12.8.2019. Katso uusin versio.

// ==UserScript==
// @name     Iwara Custom Sort
// @name:ja  Iwara Custom ソート
// @version  0.199
// @grant    GM.setValue
// @grant    GM.getValue
// @grant    GM.deleteValue
// @run-at   document-end
// @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.
// @description:ja / videos、/ images、/ subscriptions、/ usersとサイドバーのサムネイルを自動的にソートします。ソート方法はカスタマイズすることができます、一度に複数のページを読み込んでソートすることができます。
// @license  AGPL-3.0-or-later
// @require https://cdn.jsdelivr.net/npm/[email protected]/dist/sweetalert2.all.min.js#sha256-4/EaXPJ/6N3TkeW1FnAqmfV7JNVmnIFQ3bllkklPJ9U=
// @require https://unpkg.com/[email protected]/dist/loglevel.min.js#sha384-Op9lLc4V1M516+nNY8VWsadxPqqnzIpcU8UqrxIqJeVa+jbqbsAjsttJPJyACagp
// @require https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.5.2/rxjs.umd.min.js#sha256-ofji4RA7ZKBd+T/Ij8gVhRYyfvqark3dhZ/wD+/wkPg=
// @namespace https://greasyfork.org/users/245195
// ==/UserScript==

/* jshint esversion: 6 */

/******/ (function(modules) { // webpackBootstrap
/******/ 	// The module cache
/******/ 	var installedModules = {};
/******/
/******/ 	// The require function
/******/ 	function __webpack_require__(moduleId) {
/******/
/******/ 		// Check if module is in cache
/******/ 		if(installedModules[moduleId]) {
/******/ 			return installedModules[moduleId].exports;
/******/ 		}
/******/ 		// Create a new module (and put it into the cache)
/******/ 		var module = installedModules[moduleId] = {
/******/ 			i: moduleId,
/******/ 			l: false,
/******/ 			exports: {}
/******/ 		};
/******/
/******/ 		// Execute the module function
/******/ 		modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ 		// Flag the module as loaded
/******/ 		module.l = true;
/******/
/******/ 		// Return the exports of the module
/******/ 		return module.exports;
/******/ 	}
/******/
/******/
/******/ 	// expose the modules object (__webpack_modules__)
/******/ 	__webpack_require__.m = modules;
/******/
/******/ 	// expose the module cache
/******/ 	__webpack_require__.c = installedModules;
/******/
/******/ 	// define getter function for harmony exports
/******/ 	__webpack_require__.d = function(exports, name, getter) {
/******/ 		if(!__webpack_require__.o(exports, name)) {
/******/ 			Object.defineProperty(exports, name, { enumerable: true, get: getter });
/******/ 		}
/******/ 	};
/******/
/******/ 	// define __esModule on exports
/******/ 	__webpack_require__.r = function(exports) {
/******/ 		if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ 			Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ 		}
/******/ 		Object.defineProperty(exports, '__esModule', { value: true });
/******/ 	};
/******/
/******/ 	// create a fake namespace object
/******/ 	// mode & 1: value is a module id, require it
/******/ 	// mode & 2: merge all properties of value into the ns
/******/ 	// mode & 4: return value when already ns object
/******/ 	// mode & 8|1: behave like require
/******/ 	__webpack_require__.t = function(value, mode) {
/******/ 		if(mode & 1) value = __webpack_require__(value);
/******/ 		if(mode & 8) return value;
/******/ 		if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
/******/ 		var ns = Object.create(null);
/******/ 		__webpack_require__.r(ns);
/******/ 		Object.defineProperty(ns, 'default', { enumerable: true, value: value });
/******/ 		if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
/******/ 		return ns;
/******/ 	};
/******/
/******/ 	// getDefaultExport function for compatibility with non-harmony modules
/******/ 	__webpack_require__.n = function(module) {
/******/ 		var getter = module && module.__esModule ?
/******/ 			function getDefault() { return module['default']; } :
/******/ 			function getModuleExports() { return module; };
/******/ 		__webpack_require__.d(getter, 'a', getter);
/******/ 		return getter;
/******/ 	};
/******/
/******/ 	// Object.prototype.hasOwnProperty.call
/******/ 	__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/ 	// __webpack_public_path__
/******/ 	__webpack_require__.p = "";
/******/
/******/
/******/ 	// Load entry module and return exports
/******/ 	return __webpack_require__(__webpack_require__.s = 4);
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ (function(module, exports) {

module.exports = log;

/***/ }),
/* 1 */
/***/ (function(module, exports) {

module.exports = rxjs;

/***/ }),
/* 2 */
/***/ (function(module, exports) {

module.exports = Swal;

/***/ }),
/* 3 */
/***/ (function(module, exports) {

module.exports = rxjs.operators;

/***/ }),
/* 4 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
__webpack_require__.r(__webpack_exports__);

// EXTERNAL MODULE: external "Swal"
var external_Swal_ = __webpack_require__(2);
var external_Swal_default = /*#__PURE__*/__webpack_require__.n(external_Swal_);

// EXTERNAL MODULE: external "log"
var external_log_ = __webpack_require__(0);

// CONCATENATED MODULE: ./src/timeout/index.ts
/* harmony default export */ var timeout = ((delay) => new Promise((resolve) => {
    setTimeout(resolve, delay);
}));

// EXTERNAL MODULE: external "rxjs"
var external_rxjs_ = __webpack_require__(1);

// EXTERNAL MODULE: external "rxjs.operators"
var external_rxjs_operators_ = __webpack_require__(3);

// CONCATENATED MODULE: ./src/sort_component/index.ts


const createNumberInput = (value, min, max, step, width) => {
    const input = document.createElement('input');
    input.value = value.toString();
    input.min = min.toString();
    input.max = max.toString();
    input.step = step.toString();
    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 createTextInput = (text, maxLength, size) => {
    const input = document.createElement('input');
    input.value = text;
    input.maxLength = maxLength;
    input.size = size;
    return input;
};
const createButton = (text) => {
    const button = document.createElement('button');
    button.innerHTML = text;
    return button;
};
/* harmony default export */ var sort_component = (class {
    constructor(initialSortValue, defaultSortValue, pageCount) {
        this.lable1 = createSpan('', 'white');
        this.loadedPageCount = 0;
        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, 10));
            lable2.innerHTML = 'pages loaded. Refresh to apply the change';
        });
        this.sortValueInput = createTextInput(initialSortValue, 120, 60);
        this.sortValueInput.classList.add('form-text');
        this.sortButton = createButton('Sort');
        this.sort$ = Object(external_rxjs_["fromEvent"])(this.sortButton, 'click').pipe(Object(external_rxjs_operators_["map"])(() => this.sortValueInput.value));
        this.loadedPageCount = 0;
        this.sortButton.classList.add('btn', 'btn-sm', 'btn-primary');
        this.sortValueInput.addEventListener('keyup', (event) => {
            if (event.key === 'Enter') {
                this.sortButton.click();
            }
        });
        const resetDefaultButton = createButton('Default');
        Object(external_rxjs_["fromEvent"])(resetDefaultButton, 'click').subscribe(() => {
            this.sortValueInput.value = defaultSortValue;
            GM.setValue('sortValue', defaultSortValue);
        });
        resetDefaultButton.classList.add('btn', 'btn-sm', 'btn-info');
        this.UI = document.createElement('div');
        this.UI.style.display = 'inline-block';
        this.UI.append(this.sortValueInput, resetDefaultButton, this.sortButton, this.lable1, pageCountInput, lable2);
        this.UI.childNodes.forEach((node) => {
            node.style.margin = '5px 2px';
        });
        this.addLoadedPageCount();
    }
    sort() {
        this.sortButton.click();
    }
    addLoadedPageCount() {
        this.loadedPageCount += 1;
        this.lable1.innerHTML = `${this.loadedPageCount} of `;
    }
});

// CONCATENATED MODULE: ./src/index.ts
var __awaiter = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};




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 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 teasers = teaserDivs.map((div) => ({
        domElement: 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,
    }));
    // eslint-disable-next-line no-new-func
    const evalSortValue = (item, expression) => 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);
    const teaserPairs = teasers.map((teaser) => [teaser, evalSortValue(teaser, valueExpression)]);
    teaserPairs.sort((itemA, itemB) => itemB[1] - itemA[1]);
    teaserDivs.map((div) => {
        const anchor = document.createElement('div');
        div.before(anchor);
        return anchor;
    }).forEach((div, index) => div.replaceWith(teaserPairs[index][0].domElement));
};
const sortAllTeasers = (valueExpression) => {
    GM.setValue('sortValue', valueExpression);
    let sortedCount = 0;
    getTeaserGrids(document).forEach((grid) => {
        sortTeasers(grid, valueExpression);
        sortedCount += 1;
    });
    external_log_["info"](`${sortedCount} grids sorted`);
};
const getPageParam = (URL_) => {
    const params = URL_.searchParams;
    return params.has('page') ? Number.parseInt(params.get('page'), 10) : 0;
};
const currentPage = getPageParam(new URL(window.location.href));
const createAdditionalPages = (URL_, pageCount) => {
    const params = URL_.searchParams;
    let page = getPageParam(URL_);
    const pages = [];
    for (let pageLeft = pageCount; pageLeft > 0; pageLeft -= 1) {
        page += 1;
        params.set('page', page.toString());
        const nextPage = (() => (navigator.userAgent.indexOf('Firefox') > -1
            ? document.createElement('embed')
            : document.createElement('iframe')))();
        nextPage.src = URL_.toString();
        nextPage.style.display = 'none';
        external_log_["info"]('Add page:', nextPage.src);
        pages.push(nextPage);
    }
    return pages;
};
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) {
            teaserGrids[j].className = '';
            teaserGrids[j].setAttribute('originPage', currentPage.toString());
            parentGrids[i].prepend(teaserGrids[j]);
            j += 1;
        }
    }
};
const adjustPageAnchors = (container, pageCount) => {
    const changePageParam = (anchor, value) => {
        const anchorURL = new URL(anchor.href, window.location.href);
        anchorURL.searchParams.set('page', value.toString());
        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');
        const nextPageAnchor = container.querySelector('.pager-next a');
        if (nextPageAnchor) {
            changePageParam(nextPageAnchor, nextPage);
        }
        if (lastPageAnchor) {
            if (getPageParam(new URL(lastPageAnchor.href, window.location.href)) < nextPage) {
                nextPageAnchor.remove();
                lastPageAnchor.remove();
            }
        }
    }
    const loadedPageAnchors = Array.from(container.querySelectorAll('.pager-item a'))
        .filter((anchor) => {
        const page = getPageParam(new URL(anchor.href, window.location.href));
        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 = (teasersAddedMeesage) => __awaiter(undefined, void 0, void 0, function* () {
    const pageCount = yield GM.getValue('pageCount', 1);
    const defaultSortValue = '(ratio / (private * 2.0 + 1) + Math.log(likes) / 250) / (image + 8.0)';
    const initialSortValue = yield GM.getValue('sortValue', defaultSortValue);
    const sortComponent = new sort_component(initialSortValue, defaultSortValue, pageCount);
    let pages = [];
    sortComponent.sort$.subscribe((sortValue) => {
        try {
            sortAllTeasers(sortValue);
        }
        catch (error) {
            external_Swal_default.a.fire('Sorting Failed', `An error accured while sorting: ${error.toString()}`);
            pages.forEach((page) => {
                page.src = '';
                page.remove();
            });
            pages = [];
        }
    });
    document.querySelector('#user-links').after(sortComponent.UI);
    if (document.querySelectorAll('.pager').length > 0
        && document.querySelectorAll('#comments').length === 0) {
        pages = createAdditionalPages(new URL(window.location.href), pageCount - 1);
        document.body.append(...pages);
        external_log_["info"](pages);
        adjustAnchors(pageCount);
        window.addEventListener('message', (event) => {
            if (new URL(event.origin).hostname === window.location.hostname
                && event.data === teasersAddedMeesage
                && event.source !== null
                && pages.length > 0) {
                try {
                    sortComponent.sort();
                    sortComponent.addLoadedPageCount();
                    external_log_["trace"](event.source.location);
                    const loadedPage = pages[getPageParam(new URL(event.source.location.href)) - currentPage - 1];
                    loadedPage.src = '';
                    loadedPage.remove();
                }
                catch (error) {
                    external_log_["trace"](error);
                }
            }
        });
    }
    sortComponent.sort();
});
const initialize = () => __awaiter(undefined, void 0, void 0, function* () {
    try {
        external_log_["setLevel"]('trace');
        external_log_["debug"](`Parsed:${window.location}, ${document.readyState} Parent:`, window.parent);
        const teaserGrids = getTeaserGrids(document);
        if (teaserGrids.length === 0) {
            return;
        }
        const teasersAddedMeesage = 'iwara custom sort: teasersAdded';
        if (window === window.parent) {
            external_log_["debug"]('I am a Parent.');
            initParent(teasersAddedMeesage);
        }
        else {
            external_log_["debug"]('I am a child.', window.location, window.parent.location);
            yield timeout(500);
            addTeasersToParent(teaserGrids);
            window.parent.postMessage(teasersAddedMeesage, window.location.origin);
        }
    }
    catch (error) {
        external_log_["trace"](error);
    }
});
initialize();


/***/ })
/******/ ]);