Tripcolor

Colorize tripcodes, the way YOU want it

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name            Tripcolor
// @name:de         Tripcolor
// @namespace       http://www.4chan.org/
// @version         1.1.0
// @description     Colorize tripcodes, the way YOU want it
// @description:de  Färbe Tripcodes, wie es DIR gefällt
// @author          Wolvan
// @match           *://boards.4chan.org/*
// @grant           GM_getValue
// @grant           GM_setValue
// @icon            http://i.imgur.com/S2VvpO3.png
// ==/UserScript==
/* jshint -W097 */

(function () {
    'use strict';

    const US_META = {
        name: "Tripcolorizer",
        author: "Wolvan",
        version: "1.1.0",
        description: "Colorize Tripcodes"
    };

    const MENU_HTML = '<fieldset id="TripcolorizerOptions"><legend>Settings</legend>%SETTINGS</fieldset><div id="ButtonsDiv"><fieldset id="TripcolorizerTriplist"scrollable="vertical"><legend>Tripcodes</legend><div class="TripcolorizerScrolldiv">%TRIPLIST</div></fieldset><button id="TripcolorizerSaveButton">Save!</button><button id="TripcolorizerCancelButton">Cancel</button></div>';
    const MENU_CSS = '#TripcolorizerMenu{border-width:1px;border-color:#292929;border-style:solid;background-color:#222;color:#BBB;position:fixed;z-index:1002;top:50%;left:50%;transform:translate(-50%,-50%)}#TripcolorizerGlass{background-color:rgba(21,21,21,.82);width:100%;height:100%;z-index:1001;top:0;left:0;position:fixed}#TripcolorizerMenu label{display:inherit}#TripcolorizerMenu .TripcolorizerScrolldiv{max-height:500px;overflow-y:auto}#TripcolorizerMenu #ButtonsDiv{text-align:center}#TripcolorizerMenu input[type=checkbox]{display:inline-block!important}#TripcolorizerMenu .riceCheck{display:none}#TripcolorizerMenuButton{cursor:pointer}';

    Object.filter = function (obj, predicate) {
        var result = {},
            key;
        for (key in obj) {
            if (obj.hasOwnProperty(key) && predicate(obj[key])) {
                result[key] = obj[key];
            }
        }
        return result;
    };
    const utils = {
        append: {
            script: function (scriptCode, name = "TripcolorizerJS") {
                var newScript = document.createElement("script");
                newScript.id = name;
                newScript.innerHTML = scriptCode;
                document.head.appendChild(newScript);
            },
            css: function (style, name = "TripcolorizerStyle") {
                var newStyle = document.createElement("style");
                newStyle.id = name;
                newStyle.innerHTML = style;
                document.head.appendChild(newStyle);
            },
            div: function (htmlCode, name = "TripcolorizerDiv") {
                var newChild = document.createElement("div");
                newChild.id = name;
                newChild.innerHTML = htmlCode;
                document.body.appendChild(newChild);
            }
        },
        randomFrom: {
            object: function (obj) {
                var keys = Object.keys(obj);
                var randomProp = keys[Math.floor(Math.random() * keys.length)];
                return {
                    key: randomProp,
                    value: obj[randomProp]
                };
            },
            array: function (arr) {
                return arr[Math.floor(Math.random * arr.length)];
            }
        }
    };

    const defaultConfig = {
        tripcodes: {},
        settings: {
            colorNewCodes: {
                type: "checkbox",
                defaultValue: true,
                value: true,
                text: "Color unknown tripcodes",
                hint: "Automatically add new Tripcodes to the list"
            },
            colorNames: {
                type: "checkbox",
                defaultValue: true,
                value: true,
                text: "Color names",
                hint: "Color the names next to the tripcode as well"
            },
            autoColorOnUpdate: {
                type: "checkbox",
                defaultValue: true,
                value: true,
                text: "Color new posts automatically",
                hint: "When auto-polling for new replies, the new tripcode gets automatically colored"
            },
            defaultColorSystem: {
                type: "dropdown",
                defaultValue: "hex",
                value: "hex",
                values: [{
                        value: "rgb",
                        text: "RGB"
                    },
                    {
                        value: "hsl",
                        text: "HSL"
                    },
                    {
                        value: "hsv",
                        text: "HSV"
                    },
                    {
                        value: "hex",
                        text: "Hex"
                    }
                ],
                text: "Default Color Representation",
                hint: "Which color representation should new trip values use"
            }
        }
    };

    var config;
    var tmpConfig;

    var mutObs = new MutationObserver(function (mutations) {
        for (let mutation of mutations) {
            for (let node of mutation.addedNodes) {
                if (node && node.classList && node.classList.contains("postContainer")) {
                    colorTrips();
                }
            }
        }
    });

    function loadConfig() {
        try {
            var conf = GM_getValue("tripcolorizerOptions", JSON.stringify(defaultConfig));
            config = {
                tripcodes: {},
                settings: defaultConfig.settings
            };
            var parsedConfig = JSON.parse(conf);
            if (parsedConfig.tripcodes) {
                for (var trip in parsedConfig.tripcodes) {
                    config.tripcodes[trip] = parsedConfig.tripcodes[trip];
                }
            }
            if (parsedConfig.settings) {
                for (var setting in parsedConfig.settings) {
                    if (config.settings[setting]) config.settings[setting].value = parsedConfig.settings[setting].value;
                }
            }

            console.log(`[${US_META.name}]Config loaded`);
        } catch (error) {
            config = Object.assign({}, defaultConfig);
            console.log(`[${US_META.name}]Failed to load config. Using default. Error: ${error}`);
            return;
        }
    }

    // Credit for this hashing algorithm goes to http://userscripts-mirror.org/scripts/review/149083
    function worstHashAlgorithm(str) {
        var sum = 0;
        for (var ind = 0; ind < str.length; ind++) {
            sum += str.charCodeAt(ind);
        }
        return sum;
    }

    function colorTrips() {
        function hsv2hsl(hsvstr) {
            var [hue, sat, val] = hsvstr.match(/[\.\d]+%?/g);
            if (sat.indexOf("%") !== -1) {
                sat = parseFloat(sat) / 100;
            }
            if (val.indexOf("%") !== -1) {
                val = parseFloat(val) / 100;
            }
            var x = `hsl(${
                hue
            }, ${
                Math.floor((sat * val / ((hue = (2 - sat) * val) < 1 ? hue : 2 - hue)) * 100)
            }%, ${
                Math.floor((hue / 2) * 100)
            }%)`;
            return x;
        }

        function intToHexPad(int) {
            var hexnum = int.toString(16).toUpperCase();
            return (hexnum.length === 1 ? "0" + hexnum : hexnum);
        }
        var tripNodes = document.getElementsByClassName("postertrip");
        for (var tripNode of tripNodes) {
            let trip = tripNode.innerHTML;
            if (!config.tripcodes[trip] && config.settings.colorNewCodes.value) {
                let strippedTrip = trip.replace(/!/g, "");
                let baseValues = [
                    worstHashAlgorithm(strippedTrip.substr(0, 4)) % 255,
                    worstHashAlgorithm(strippedTrip.substr(4, 4)) % 255,
                    worstHashAlgorithm(strippedTrip.substr(8)) % 255
                ];
                switch (config.settings.defaultColorSystem.value) {
                    case "hsl":
                    case "hsv":
                        let hue = Math.floor((baseValues[0] / 255) * 360);
                        let sat = baseValues[1] / 255;
                        let light = baseValues[2] / 255;
                        config.tripcodes[trip] = `hsl(${hue}, ${Math.floor(sat * 100)}%, ${Math.floor(light * 100)}%)`;
                        if (config.settings.defaultColorSystem.value === "hsl") break;
                        sat *= light < 0.5 ? light : 1 - light;
                        config.tripcodes[trip] = `hsv(${
                            hue
                        }, ${
                            Math.floor((2 * sat / (light + sat)) * 100)
                        }%, ${
                            Math.floor((light + sat) * 100)
                        }%)`;
                        break;
                    case "hex":
                        config.tripcodes[trip] = `#${
                            intToHexPad(baseValues[0])
                        }${
                            intToHexPad(baseValues[1])
                        }${
                            intToHexPad(baseValues[2])
                        }`;
                        break;
                    case "rgb": // jshint ignore:line
                    default:
                        config.tripcodes[trip] = `rgb(${
                            baseValues[0]
                        }, ${
                            baseValues[1]
                        }, ${
                            baseValues[2]
                        })`;
                        break;
                }
            }
            if (config.tripcodes[trip]) {
                let targets;
                if (config.settings.colorNames.value) {
                    targets = tripNode.parentNode.children;
                } else {
                    targets = [tripNode];
                }
                for (let target of targets) {
                    if (target.tagName.toLowerCase() === "span") {
                        target.classList.add("TripcolorizerStyled");
                        target.style.setProperty("color", config.tripcodes[trip].match(/hsv/gi) ? hsv2hsl(config.tripcodes[trip]) : config.tripcodes[trip], "important");
                    }
                }
            }
        }
    }

    function uncolorTrips() {
        var allStyled = document.getElementsByClassName("TripcolorizerStyled");
        while (allStyled.length > 0) {
            allStyled[0].style.removeProperty("color");
            allStyled[0].classList.remove("TripcolorizerStyled");
        }
    }

    function showMenu() {
        tmpConfig = {};
        tmpConfig.settings = Object.assign({}, config.settings);
        tmpConfig.tripcodes = Object.assign({}, config.tripcodes);

        function closeMenu() {
            document.getElementById("TripcolorizerGlass").remove();
            document.getElementById("TripcolorizerMenu").remove();
        }

        function showGlass() {
            var name = "TripcolorizerGlass";
            var oldelem = document.getElementById(name);
            if (oldelem) oldelem.remove();
            utils.append.div("", name);
        }

        const STRING_TEMPLATES = {
            checkbox: '<td><input class="TripcolorizerSetting"title="%SETTINGSHINT"name="%SETTINGNAME"type="checkbox"id="Tripcolorizer_%SETTINGNAME"%SETTINGSET></td><td><label for="Tripcolorizer_%SETTINGNAME"title="%SETTINGSHINT">%SETTINGTEXT</label></td>',
            dropdown: '<td><select class="TripcolorizerSetting"title="%SETTINGSHINT"name="%SETTINGNAME" id="Tripcolorizer_%SETTINGNAME">%OPTIONS</select></td><td><label for="Tripcolorizer_%SETTINGNAME"title="%SETTINGSHINT">%SETTINGTEXT</label></td>',


            tripcodes: `<tr class="TripcolorizerTrip"><td><input type="text"class="TripcolorizerTripcode"value="%TRIPCODE"placeholder="%TRIPCODE"></td><td><input type="text"class="TripcolorizerTripcolor"value="%TRIPCOLOR"placeholder="%TRIPCOLOR"></td><td><button class="TripcolorizerDeletThis"data-trip="%TRIPCODE">Delete</button></td></tr>`
        };

        function getTriplist() {
            var triplist = '';
            for (var trip in tmpConfig.tripcodes) {
                triplist += STRING_TEMPLATES.tripcodes.replace(/%TRIPCODEEXAMPLE/g, "Tripcode (+! or !!)")
                    .replace(/%TRIPCOLOREXAMPLE/g, "CSS Style for Trip")
                    .replace(/%TRIPCODE/g, trip)
                    .replace(/%TRIPCOLOR/g, tmpConfig.tripcodes[trip]);
            }
            return triplist;
        }

        function addTrip() {
            var trip = document.getElementById("TripcolorizerNewTrip").value;
            var color = document.getElementById("TripcolorizerNewTripColor").value;

            if (trip && color) {
                tmpConfig.tripcodes[trip] = color;
                document.getElementById("TripcolorizerNewTrip").value = "";
                document.getElementById("TripcolorizerNewTripColor").value = "";
                document.getElementById("TripcolorizerTriplistTable").innerHTML = getTriplist();
                bindTripDeleteButtons();
            }
        }

        function bindTripDeleteButtons() {
            var deleteButtons = document.getElementsByClassName("TripcolorizerDeletThis");
            for (var button of deleteButtons) {
                button.addEventListener("click", function () {
                    delete tmpConfig.tripcodes[this.dataset.trip];
                    document.getElementById("TripcolorizerTriplistTable").innerHTML = getTriplist();
                    bindTripDeleteButtons();
                });
            }
        }

        function showMenu() {

            var name = "TripcolorizerMenu";
            var oldelem = document.getElementById(name);
            if (oldelem) oldelem.remove();
            var settingsHTML = '<table id="TripcolorizerSettingsTable">';

            var settingControl = "";

            var setting;
            for (var settingName in tmpConfig.settings) {
                setting = tmpConfig.settings[settingName];
                switch (setting.type) {
                    case "checkbox":
                        settingControl = STRING_TEMPLATES.checkbox.replace(/%SETTINGNAME/g, settingName)
                            .replace(/%SETTINGTEXT/g, setting.text)
                            .replace(/%SETTINGSHINT/g, setting.hint || "")
                            .replace(/%SETTINGSET/g, (setting.value ? 'checked="checked"' : ""));
                        break;
                    case "dropdown":
                        settingControl = STRING_TEMPLATES.dropdown.replace(/%SETTINGNAME/g, settingName)
                            .replace(/%SETTINGTEXT/g, setting.text)
                            .replace(/%SETTINGSHINT/g, setting.hint || "")
                            .replace(/%OPTIONS/g, setting.values.map(function (item) {
                                return `<option value="${item.value}"${item.value === setting.value ? "selected" : ""}>${item.text}</option>`;
                            }));
                        break;
                    default:
                        break;
                }
                settingsHTML += "<tr>" + settingControl + "</tr>";
            }

            settingsHTML += "</table>";

            var triplist = '<table id="TripcolorizerTriplistTable">' + getTriplist();
            triplist += "</table>";
            triplist += '<table id="TripcolorizerTriplistAddTable">';
            triplist += '<tr><td><input type="text"name="NewTrip"id="TripcolorizerNewTrip"placeholder="%TRIPCODEEXAMPLE"></td><td><input type="text"name="NewTripColor"id="TripcolorizerNewTripColor"placeholder="%TRIPCOLOREXAMPLE"><td><button id="TripcolorizerAddTrip">Add</button></td></td></tr>';
            triplist += "</table>";

            utils.append.div(MENU_HTML.replace(/%SETTINGS/g, settingsHTML)
                .replace(/%TRIPLIST/g, triplist)
                .replace(/%TRIPCODEEXAMPLE/g, "Tripcode (+! or !!)")
                .replace(/%TRIPCOLOREXAMPLE/g, "CSS Style for trip"), name);
        }
        showGlass();
        showMenu();

        document.getElementById("TripcolorizerAddTrip").addEventListener("click", addTrip);
        bindTripDeleteButtons();

        document.getElementById("TripcolorizerGlass").addEventListener("click", closeMenu);
        document.getElementById("TripcolorizerCancelButton").addEventListener("click", closeMenu);
        document.getElementById("TripcolorizerSaveButton").addEventListener("click", function () {
            var settingElements = document.getElementsByClassName("TripcolorizerSetting");
            for (var setting of settingElements) {
                switch (setting.tagName.toLowerCase()) {
                    case "input":
                        switch (setting.type.toLowerCase()) {
                            case "checkbox":
                                tmpConfig.settings[setting.name].value = setting.checked;
                                break;
                            default:
                                break;
                        }
                        break;
                    case "select":
                        tmpConfig.settings[setting.name].value = setting.value;
                        break;
                    default:
                        break;
                }
            }
            var tripcodeListElements = document.getElementsByClassName("TripcolorizerTrip");
            for (var tripRow of tripcodeListElements) {
                let children = tripRow.children;
                let tripcode;
                let tripcolor;
                for (let child of children) {
                    child = child.children[0];
                    if (child.classList.contains("TripcolorizerTripcode")) {
                        if (child.value) tripcode = child.value;
                    } else if (child.classList.contains("TripcolorizerTripcolor")) {
                        if (child.value) tripcolor = child.value;
                    }
                }
                if (tripcode && tripcolor) tmpConfig.tripcodes[tripcode] = tripcolor;
            }
            config = Object.assign({}, tmpConfig);
            GM_setValue("tripcolorizerOptions", JSON.stringify(config));
            console.log(`[${US_META.name}]Saved Settings to Tripcolorizer`);
            console.log(`[${US_META.name}]Re-coloring Trips`);
            uncolorTrips();
            colorTrips();
            console.log(`[${US_META.name}]${config.settings.autoColorOnUpdate.value ? "Enabling" : "Disabling"} update observer`);
            if (config.settings.autoColorOnUpdate.value) {
                mutObs.observe(document.getElementsByClassName("thread")[0], {
                    childList: true,
                    subtree: true
                });
            } else {
                mutObs.disconnect();
            }
            closeMenu();
        });
    }

    function keyDownHandler(event) {
        if (event.ctrlKey && event.which === 76) {
            showMenu();
        }
    }

    function init() {
        console.log(`[${US_META.name}]Initializing ${US_META.name} v${US_META.version} by ${US_META.author}`);
        console.log(`[${US_META.name}]Injecting CSS`);
        utils.append.css(MENU_CSS, "TripcolorizerMenuStyling");
        console.log(`[${US_META.name}]Loading config`);
        loadConfig();
        console.log(`[${US_META.name}]Hooking Keyboard Event`);
        document.addEventListener('keydown', keyDownHandler);
        console.log(`[${US_META.name}]Coloring ${!config.settings.colorNewCodes ? "known " : ""}tripcodes`);
        colorTrips();
        if (config.settings.autoColorOnUpdate.value) {
            console.log(`[${US_META.name}]Enabling update observer`);
            mutObs.observe(document.getElementsByClassName("thread")[0], {
                childList: true,
                subtree: true
            });
        }
    }

    init();
})();