NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==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(); })();