Greasy Fork tweaks

opens pages of scripts from lists in a new tab and makes the user interface more compact, informative and interactive

当前为 2020-06-02 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name            Greasy Fork tweaks
// @namespace       almaceleste
// @version         0.5.1
// @description     opens pages of scripts from lists in a new tab and makes the user interface more compact, informative and interactive
// @description:ru  открывает страницы скриптов из списков в новой вкладке и делает пользовательский интерфейс более компактным, информативным и интерактивным
// @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 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-child > h2';

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: 530px;
    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: {
        version: {
            section: ['', 'Script list options (own and other pages)'],
            label: 'add script version number in the list of scripts',
            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},
        },
        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,
        },
        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 getupdates(url, target){
    fetch(url).then((response) => {
        response.json().then((json) => {
            const data = Object.values(json);

            const updatesperiods = GM_config.get('updatesperiods');

            for (const period in updatesperiods) {
                const updates = sumlast(data, updatesperiods[period], 'update_checks');
                $('<span></span>', {
                    title: period,
                }).text(updates).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 list, target, url;
    switch (page) {
        case 'user':
        case 'search':
            list = listitem;
            target = scriptstats;
            break;        
        case 'script':
            list = `#script-meta`;
            target = `#script-stats`;
            url = `${window.location.href}/stats.json`;
            break;
        default:
            break;
    }
    $(list).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,dd,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);

            getupdates(url, $(updatechecks));
        }
    });
}

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

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

(function() {
    'use strict';

    $(document).ready(() => {
        router(window.location.pathname);
    });
})();