Raw Source
almaceleste / Greasy Fork tweaks

// ==UserScript==
// @name            Greasy Fork tweaks
// @namespace       almaceleste
// @version         0.6.3
// @description     various tweaks for greasyfork.org site for enhanced usability and additional features
// @description:ru  различные твики для сайта greasyfork.org для повышения удобства использования и дополнительных функций
// @author          (ɔ) almaceleste  (https://almaceleste.github.io)
// @license         AGPL-3.0-or-later; http://www.gnu.org/licenses/agpl.txt
// @icon            https://greasyfork.org/assets/blacklogo16-bc64b9f7afdc9be4cbfa58bdd5fc2e5c098ad4bca3ad513a27b15602083fd5bc.png
// @icon64          https://greasyfork.org/assets/blacklogo96-e0c2c76180916332b7516ad47e1e206b42d131d36ff4afe98da3b1ba61fd5d6c.png

// @homepageURL     https://greasyfork.org/en/users/174037-almaceleste
// @homepageURL     https://openuserjs.org/users/almaceleste
// @homepageURL     https://github.com/almaceleste/userscripts
// @supportURL      https://github.com/almaceleste/userscripts/issues
// @updateURL       https://github.com/almaceleste/userscripts/raw/master/src/Greasy_Fork_tweaks.user.js
// @downloadURL     https://github.com/almaceleste/userscripts/raw/master/src/Greasy_Fork_tweaks.user.js
// @downloadURL     https://openuserjs.org/install/almaceleste/Greasy_Fork_tweaks.user.js

// @require         https://code.jquery.com/jquery-3.3.1.js
// @require         https://code.jquery.com/ui/1.12.1/jquery-ui.js
// @require         https://openuserjs.org/src/libs/sizzle/GM_config.js
// @grant           GM_getValue
// @grant           GM_setValue
// @grant           GM_registerMenuCommand
// @grant           GM_openInTab
// @grant           GM_getResourceText

// @resource        css https://github.com/almaceleste/userscripts/raw/master/css/default.css

// @match           https://greasyfork.org/*/users/*
// @match           https://greasyfork.org/*/scripts*
// ==/UserScript==

// ==OpenUserJS==
// @author almaceleste
// ==/OpenUserJS==

const route = {};
route.userpage = /^\/.*\/users\/.*/;
route.scriptpage = /^\/.*\/scripts\/[^\/]*$/; //(?!\/)*/; //.*/;
route.searchpage = /^\/.*\/scripts$/;

const maincontainer = 'body > .width-constraint';
const listitem = '.script-list > li';
const separator = '.name-description-separator';
const scriptversion = 'data-script-version';
const scriptrating = 'dd.script-list-ratings';
const scriptstats = '.inline-script-stats';
const dailyinstalls = '.script-list-daily-installs';
const totalinstalls = '.script-list-total-installs';
const createddate = '.script-list-created-date';
const updateddate = '.script-list-updated-date';

const scripturl = 'article h2 a';

const userprofile = {};
userprofile.path = '#user-profile';
userprofile.header = 'body > div.width-constraint > section:first-of-type > h2:first-of-type';

const sections = {};
sections.controlpanel = '#control-panel';
sections.discussions = '#user-discussions-on-scripts-written';
sections.scriptsets = 'section:has(h3:contains("Script Sets"))';

const configId = 'greasyforktweaksCfg';
const iconUrl = GM_info.script.icon64;
const pattern = {};
pattern[`#${configId}`] = /#configId/g;
pattern[`${iconUrl}`] = /iconUrl/g;

let css = GM_getResourceText('css');
Object.keys(pattern).forEach((key) => {
    css = css.replace(pattern[key], key);
});
const windowcss = css;
const iframecss = `
    height: 590px;
    width: 435px;
    border: 1px solid;
    border-radius: 3px;
    position: fixed;
    z-index: 9999;
`;

GM_registerMenuCommand(`${GM_info.script.name} Settings`, () => {
	GM_config.open();
    GM_config.frame.style = iframecss;
});

GM_config.init({
    id: `${configId}`,
    title: `${GM_info.script.name} ${GM_info.script.version}`,
    fields: {
        width: {
            section: ['', 'All pages options'],
            label: 'page width',
            labelPos: 'left',
            type: 'text',
            default: '70%',
        },
        version: {
            label: 'add script version number',
            labelPos: 'right',
            type: 'checkbox',
            default: true,
        },
        ratingscore: {
            label: 'display script rating score',
            labelPos: 'right',
            type: 'checkbox',
            default: true,
        },
        updates: {
            label: 'display update checks information',
            labelPos: 'right',
            type: 'checkbox',
            default: true,
        },
        updatesperiods: {
            type: 'multiselect',
            options: {
                daily: 1,
                weekly: 7,
                monthly: 30,
                total: 0
            },
            default: {daily: 1, weekly: 7, monthly: 30, total: 0},
        },
        installs: {
            label: 'display alternative installs information',
            labelPos: 'right',
            type: 'checkbox',
            default: true,
        },
        installsperiods: {
            type: 'multiselect',
            options: {
                daily: 1,
                weekly: 7,
                monthly: 30,
                total: 0
            },
            default: {daily: 1, weekly: 7, monthly: 30, total: 0},
        },
        compact: {
            label: 'compact script information',
            labelPos: 'right',
            type: 'checkbox',
            default: true,
        },
        userprofile: {
            section: ['', 'User page options (own page and other users`)'],
            label: 'collapse user profile info on user page',
            labelPos: 'right',
            type: 'checkbox',
            default: true,
        },
        controlpanel: {
            label: 'collapse control panel on user page',
            labelPos: 'right',
            type: 'checkbox',
            default: true,
        },
        discussions: {
            label: 'collapse discussions on user page',
            labelPos: 'right',
            type: 'checkbox',
            default: true,
        },
        scriptsets: {
            label: 'collapse script sets on user page',
            labelPos: 'right',
            type: 'checkbox',
            default: true,
        },
        displayimage: {
            label: 'display script image (experimental)',
            labelPos: 'right',
            type: 'checkbox',
            default: true,
        },
        newtab: {
            section: ['', 'Other options'],
            label: 'open script page in new tab',
            labelPos: 'right',
            type: 'checkbox',
            default: true,
        },
        background: {
            label: 'open new tab in background',
            labelPos: 'right',
            type: 'checkbox',
            default: false,
        },
        insert: {
            label: 'insert new tab next to the current instead of the right end',
            labelPos: 'right',
            type: 'checkbox',
            default: true,
        },
        setParent: {
            label: 'return to the current tab after new tab closed',
            labelPos: 'right',
            type: 'checkbox',
            default: true,
        },
        support: {
            section: ['', 'Support'],
            label: 'almaceleste.github.io',
            title: 'more info on almaceleste.github.io',
            type: 'button',
            click: () => {
                GM_openInTab('https://almaceleste.github.io', {
                    active: true,
                    insert: true,
                    setParent: true
                });
            }
        },
    },
    types: {
        multiselect: {
            default: {},
            toNode: function() {
                let field = this.settings,
                    value = this.value,
                    options = field.options,
                    id = this.id,
                    configId = this.configId,
                    labelPos = field.labelPos,
                    create = this.create;

                // console.log('toNode:', field, value, options);
                function addLabel(pos, labelEl, parentNode, beforeEl) {
                    if (!beforeEl) beforeEl = parentNode.firstChild;
                    switch (pos) {
                        case 'right': case 'below':
                            if (pos == 'below')
                                parentNode.appendChild(create('br', {}));
                            parentNode.appendChild(labelEl);
                            break;
                        default:
                            if (pos == 'above')
                                parentNode.insertBefore(create('br', {}), beforeEl);
                            parentNode.insertBefore(labelEl, beforeEl);
                    }
                }

                let retNode = create('div', { 
                        className: 'config_var multiselect',
                        id: `${configId}_${id}_var`,
                        title: field.title || ''
                    }),
                    firstProp;
            
                // Retrieve the first prop
                for (let i in field) { firstProp = i; break; }
            
                let label = field.label ? create('label', {
                        className: 'field_label',
                        id: `${configId}_${id}_field_label`,
                        for: `${configId}_field_${id}`,
                    }, field.label) : null;
      
                let wrap = create('ul', {
                    id: `${configId}_field_${id}`
                });
                this.node = wrap;

                for (const key in options) {
                    // console.log('toNode:', key);
                    const inputId = `${configId}_${id}_${key}_checkbox`;
                    const li = wrap.appendChild(create('li', {
                    }));
                    li.appendChild(create('input', {
                        checked: value.hasOwnProperty(key),
                        id: inputId,
                        type: 'checkbox',
                        value: options[key],
                    }));
                    li.appendChild(create('label', {
                        className: 'option_label',
                        for: inputId,
                    }, key));
                }

                retNode.appendChild(wrap);

                if (label) {
                    // If the label is passed first, insert it before the field
                    // else insert it after
                    if (!labelPos)
                        labelPos = firstProp == "label" ? "left" : "right";
              
                    addLabel(labelPos, label, retNode);
                }
                
                return retNode;
            },
            toValue: function() {
                let node = this.node,
                    id = node.id,
                    options = this.settings.options,
                    rval = {};

                // console.log('toValue:', node, options, this);

                if (!node) return rval;

                let nodelist = node.querySelectorAll(`#${id} input:checked`);
                // console.log('nodelist:', document.querySelectorAll(`#${id} input:checked`), nodelist);
                nodelist.forEach((input) => {
                    // console.log('toValue:', input);
                    const value = input.value;
                    const key = Object.keys(options).find((key) => options[key] == value);
                    rval[key] = value;
                });

                // console.log('toValue:', rval);
                return rval;
            },
            reset: function() {
                let node = this.node,
                    values = this.default;

                // console.log('reset:', node, values, Object.values(values));
                const inputs = node.getElementsByTagName('input');
                for (const index in inputs) {
                    const input = inputs[index];
                    // console.log('reset:', input.value, Object.values(values).includes(input.value) || Object.values(values).includes(+input.value));
                    if (Object.values(values).includes(input.value) || Object.values(values).includes(+input.value)) {
                        if (!input.checked) input.click();
                    }
                    else {
                        if (input.checked) input.click();
                    }
                }
            }
        }
    },
    css: windowcss,
    events: {
        save: function() {
            GM_config.close();
        }
    },
});

function arrow(element){
    const arrow = $(`
        <svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
            <style>
                .collapsed {
                    transform: rotate(0deg);
                }
                .expanded {
                    transform: rotate(180deg);
                }
            </style>
            <text x='0' y='18'>▼</text>
        </svg>
    `).css({
        fill: 'whitesmoke',
        height: '20px',
        width: '30px',
    });
    $(element).append(arrow);
}

function collapse(element, header){
    $(element).css({
        cursor: 'pointer',
    });
    arrow($(element).find(header));
    $(element).accordion({
        collapsible: true,
        active: false,
        beforeActivate: () => {
            rotate($(element).find('svg'));
        }
    });
}

function rotate(element){
    if ($(element).hasClass('expanded')) {
        $(element).animate({
            transform: 'rotate(0deg)',
        });
    }
    else {
        $(element).animate({
            transform: 'rotate(180deg)',
        });
    }
    $(element).toggleClass('expanded');
}

function compact(first, second){
    $('dt' + first).each(function(){
        $(this).css('display','none');
        $(this).siblings('dt' + second).find('span').append(' (' + $(this).find('span').text() + ')');
    });
    $('dd' + first).each(function(){
        $(this).css('display','none');
        $(this).siblings('dd' + second).find('span').append(' (' + $(this).find('span').text() + ')');
    });
}

function newtaber(e){
    const options = {active: !GM_config.get('background'), insert: GM_config.get('insert'), setParent: GM_config.get('setParent')};
    e.preventDefault();
    e.stopPropagation();
    GM_openInTab(e.target.href, options);
}

function getjson(url){
    fetch(url).then((response) => {
        // console.log('getjson:', response);
        response.json().then((json) => {
            console.log('getjson:', json);
        });
    });
}

function sumlast(array, number, prop){
    if (number != 0) {
        array = array.slice(-number);
    }
    let result = array.reduce((sum, next) => {
        return sum + next[prop];
    }, 0);
    return result;
}

function getjsondata(url, prop, periods, target){
    fetch(url).then((response) => {
        response.json().then((json) => {
            const data = Object.values(json);

            for (const period in periods) {
                const result = sumlast(data, periods[period], prop);
                $('<span></span>', {
                    title: period,
                }).text(result).appendTo(target);
            }
        });
    });
}

function doCompact(){
    if (GM_config.get('compact')){
        $(scriptstats).children().css('width','auto');
        compact(totalinstalls, dailyinstalls);
        compact(updateddate, createddate);
    }
}

function doRating(page){
    switch (page) {
        case 'user':
        case 'search':
            $(scriptrating).each(function(){
                let rating = $(this).attr('data-rating-score');
                $(this).children('span').after(` - ${rating}`);
            });
            break;
        case 'script':
            $(scriptrating).each(function(){
                const author = '#script-stats > .script-show-author > span > a';
                const url = `${window.location.origin}${$(author).attr('href')}`;
                const scriptId = '#script-content > .script-in-sets > input[name="script_id"]';
                const id = $(scriptId).val();

                fetch(url).then((response) => {
                    response.text().then((data) => {
                        const parser = new DOMParser();
                        const doc = parser.parseFromString(data, 'text/html');
                        const el = doc.querySelector(`#user-script-list li[data-script-id="${id}"]`);

                        $(this).children('span').after(` - ${el.dataset.scriptRatingScore}`);
                    });
                });
            });
            break;
        default:
            break;
    }
}

function doCollapse(){
    Object.keys(sections).forEach((section) => {
        if (GM_config.get(section)) {
            collapse(sections[section], 'header h3');
        }
    });
}

function doProfile(){
    $(userprofile.path).slideUp();
    arrow($(userprofile.header));
    $(userprofile.header).css({
        cursor: 'pointer',
    })
    .click(function(){
        $(userprofile.path).slideToggle();
        rotate($(this).find('svg'));
    });
}

function doList(){
    const version = GM_config.get('version');
    const newtab = GM_config.get('newtab');
    $(listitem).each(function(){
        if (version){
            $(this).find(separator).before(` ${$(this).attr(scriptversion)}`);
        }
        if (newtab){
            $(this).find(separator).prev('a').click(newtaber);
        }
    });
}

function doUpdates(page){
    let parent, target, url;
    switch (page) {
        case 'user':
        case 'search':
            parent = listitem;
            target = scriptstats;
            break;        
        case 'script':
            parent = `#script-meta`;
            target = `#script-stats`;
            url = `${window.location.href}/stats.json`;
            break;
        default:
            break;
    }
    $(parent).each((index, item) => {
        $(item).css({
            maxWidth: 'unset',
        });
        const stats = $(item).find(target);
        if (page != 'script') url = `${$(item).find(scripturl).attr('href')}/stats.json`;

        const updatesperiods = GM_config.get('updatesperiods');
        if (Object.keys(updatesperiods).length > 0) {
            const dt = $('<dt></dt>', {
                class: 'script-list-update-checks',
                style: 'cursor: default',
                width: 'auto',
            });
            
            let text = 'Updates (';
            let title = 'Update checks (';
            for (const period in updatesperiods) {
                text += `${period.charAt(0)}|`;
                title +=`${period}|`;
            };
            text = text.replace(/\|$/, '):');
            title = title.replace(/\|$/, ')');
            dt.text(text).attr('title', title).append(`
            <style>
                .inline-script-stats dt,
                .inline-script-stats dd,
                .inline-script-stats span {
                    cursor: default;
                    width: auto !important;
                }
                .script-list-update-checks span {
                    padding: 0 5px;
                }
                .script-list-update-checks span:not(:last-child) {
                    border-right: 1px dotted whitesmoke;
                }
            </style>`).appendTo($(stats));

            const updatechecks = $('<dd></dd>', {
                class: 'script-list-update-checks',
            });
            $(stats).append(updatechecks);

            getjsondata(url, 'update_checks', updatesperiods, $(updatechecks));
        }
    });
}

function doInstalls(page){
    let daily, parent, target, total, url;
    switch (page) {
        case 'user':
        case 'search':
            daily = dailyinstalls;
            parent = listitem;
            target = scriptstats;
            total = totalinstalls;
            break;        
        case 'script':
            daily = '.script-show-daily-installs';
            parent = `#script-meta`;
            target = `#script-stats`;
            total = '.script-show-total-installs';
            url = `${window.location.href}/stats.json`;
            break;
        default:
            break;
    }
    $(daily).css({
        display: 'none',
    });
    $(total).css({
        display: 'none',
    });

    $(parent).each((index, item) => {
        $(item).css({
            maxWidth: 'unset',
        });
        const stats = $(item).find(target);
        if (page != 'script') url = `${$(item).find(scripturl).attr('href')}/stats.json`;

        const installsperiods = GM_config.get('installsperiods');
        if (Object.keys(installsperiods).length > 0) {
            const dt = $('<dt></dt>', {
                class: 'script-list-installs',
                style: 'cursor: default',
                width: 'auto',
            });
            
            let text = 'Installs (';
            let title = 'Installs (';
            for (const period in installsperiods) {
                text += `${period.charAt(0)}|`;
                title +=`${period}|`;
            };
            text = text.replace(/\|$/, '):');
            title = title.replace(/\|$/, ')');
            dt.text(text).attr('title', title).append(`
            <style>
                .inline-script-stats dt,
                .inline-script-stats dd,
                .inline-script-stats span {
                    cursor: default;
                    width: auto !important;
                }
                .script-list-installs span {
                    padding: 0 5px;
                }
                .script-list-installs span:not(:last-child) {
                    border-right: 1px dotted whitesmoke;
                }
            </style>`).appendTo($(stats));

            const installs = $('<dd></dd>', {
                class: 'script-list-installs',
            });
            $(stats).append(installs);

            getjsondata(url, 'installs', installsperiods, $(installs));
        }
    });
}

function isImage(url){
    const types = ['apng', 'bmp', 'gif', 'ico', 'jfi', 'jfif', 'jif', 'jpe', 'jpeg', 'jpg', 'pjp', 'pjpeg', 'png', 'psd', 'svg', 'tif', 'tiff', 'webp'];
    const ext = url.split('/').pop().split('#').shift().split('?').shift().split('.').pop();
    return types.includes(ext);
}

function displayImage(){
    $(listitem).each((index, item) => {
        const url = `${$(item).find(scripturl).attr('href')}`;
        const height = $(item).height();

        $(item).children('article').css({
            display: 'inline-block',
            margin: '0',
            width: '75%',
        });
        const div = $('<div></div>').appendTo($(item)).css({
            display: 'inline-block',
            height: `${height}px`,
            float: 'right',
            margin: '0',
            overflow: 'hidden',
            padding: '5px',
            width: '20%',
        });
        fetch(url).then((response) => {
            response.text().then((data) => {
                const parser = new DOMParser();
                const doc = parser.parseFromString(data, 'text/html');
                const el = doc.querySelector(`#additional-info img:first-child`);
                let src = el.getAttribute('src');

                if (el.parentElement.hasAttribute('href')) {
                    const href = el.parentElement.getAttribute('href');
                    if (isImage(href)) src = href;
                }

                const width = $(div).width();
                const img = $('<img/>', {
                    src: src,
                    width: `${width}px`,
                });
                $(div).append(`
                    <style>
                        ${listitem}::after {
                            clear: both;
                        }
                    </style>
                `).append(img);
            });
        });
    });
}

function router(path){
    const ratingscore = GM_config.get('ratingscore');
    const displayimage = GM_config.get('displayimage');
    const installs = GM_config.get('installs');
    const updates = GM_config.get('updates');
    const userprofile = GM_config.get('userprofile');

    switch (true) {
        case route.userpage.test(path):
            console.log('router:', 'user', path);
            if (userprofile) doProfile();
            doCollapse();
            doCompact();
            if (ratingscore) doRating('user');
            doList();
            if (installs) doInstalls('user');
            if (updates) doUpdates('user');
            if (displayimage) displayImage();
            break;
        case route.searchpage.test(path):
            console.log('router:', 'search', path);
            if (ratingscore) doRating('search');
            doCompact();
            doList();
            if (installs) doInstalls('search');
            if (updates) doUpdates('search');
            if (displayimage) displayImage();
            break;
        case route.scriptpage.test(path):
            console.log('router:', 'script', path);
            if (ratingscore) doRating('script');
            if (installs) doInstalls('script');
            if (updates) doUpdates('script');
            break;
        default:
            console.log('router:', 'default', path);
            break;
    }
}

(function() {
    'use strict';

    $(document).ready(() => {
        const width = GM_config.get('width');
        $(maincontainer).css({
            maxWidth: width,
        });

        router(window.location.pathname);
    });
})();