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