Greasy Fork tweaks

various tweaks for greasyfork.org site for enhanced usability and additional features

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

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

// @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);
    });
})();