/* global PleromaMod */ class ColorDetector { constructor (image) { this.color = { r: 0, g: 0, b: 0 }; this.image = image; } componentToHex (c) { const hex = Math.max(Math.min(c, 255), 0).toString(16); return hex.length === 1 ? "0" + hex : hex; } getHexColor (offset) { if (!offset) { offset = { r: 0, g: 0, b: 0 }; } return "#" + this.componentToHex(this.color.r + offset.r) + this.componentToHex(this.color.g + offset.g) + this.componentToHex(this.color.b + offset.b); } detect () { return new Promise((resolve) => { const blockSize = 5; const canvas = document.createElement("canvas"); const context = canvas.getContext && canvas.getContext("2d"); if (!context) { console.warn("can't get context of avatar"); resolve(this.color); return; } const height = canvas.height = this.image.naturalHeight || this.image.offsetHeight || this.image.height; const width = canvas.width = this.image.naturalWidth || this.image.offsetWidth || this.image.width; context.drawImage(this.image, 0, 0); let data; try { data = context.getImageData(0, 0, width, height); } catch (e) { console.error("can't get avatar image data"); console.error(e); resolve(this.color); return; } let i = -4; let count = 0; const length = data.data.length; const rgb = { r: 0, g: 0, b: 0 }; while ((i += blockSize * 4) < length) { ++count; rgb.r += data.data[i]; rgb.g += data.data[i + 1]; rgb.b += data.data[i + 2]; } this.color.r = ~~(rgb.r / count); this.color.g = ~~(rgb.g / count); this.color.b = ~~(rgb.b / count); resolve(this.color); }); } } class PleromaCat { constructor (handle, type, config) { config = config || {}; this.type = type || "cat"; this.handle = handle; this.colors = { backgroundColor: "#000000", borderColor: "#000000" }; this.config = { "nya": { enabled: false, replacer: {} } }; this.loadConfig(config); } loadConfig (json) { if (!json.nya) { return; } this.config.nya.enabled = json.nya.enabled; if (this.config.nya.enabled) { this.config.nya.matcher = json.nya.matcher || this.config.nya.matcher; this.config.nya.replacer.source = json.nya.replacer.source || this.config.nya.replacer.source; this.config.nya.replacer.dest = json.nya.replacer.dest || this.config.nya.replacer.dest; } } getClassName () { return "USER____" + this.handle.replace(/@/g, "_AT_").replace(/\./g, "_"); } makeCat (element) { element = element || document; if (element.querySelectorAll) { const posts = element.querySelectorAll("." + this.getClassName()); this.makeCatByClassName("user-info"); this.makeCatByClassName("basic-user-card"); posts.forEach((currentPost) => { this.makeCatByElement(currentPost); this.nyaByPost(currentPost); }); } } makeCatByClassName (className, usernameClass) { className = className || "user-info"; usernameClass = usernameClass || "user-screen-name"; const userInfos = document.querySelectorAll("." + className); userInfos.forEach((userInfo) => { if (userInfo.querySelector && !/cat$/.test(userInfo.innerText)) { const handle = userInfo.querySelector("." + usernameClass); const regexHandle = new RegExp(this.handle, "i"); if (handle) { if (regexHandle.test(handle.innerText)) { this.makeCatByElement(userInfo); } } } }); } makeCatByElement (element) { if (element.querySelectorAll) { element.classList.add("catified"); element.classList.add(this.type); const avatars = element.querySelectorAll(".Avatar"); avatars.forEach((avatar) => { if (avatar.style) { if (this.colors.borderColor === "#000000") { this.detectColors(avatar); } avatar.style.backgroundColor = this.colors.backgroundColor; avatar.style.borderColor = this.colors.borderColor; } }); } } nyaByPost (element) { if (element.querySelectorAll && this.config.nya.enabled && element.classList.contains("cat")) { const contents = element.querySelectorAll(".StatusContent"); contents.forEach((content) => { if (content.innerHTML) { const regex = new RegExp(this.config.nya.matcher, "g"); let match; while ((match = regex.exec(content.innerHTML)) !== null) { const source = match[0]; const dest = source.replace( new RegExp(this.config.nya.replacer.source, "g"), this.config.nya.replacer.dest ); content.innerHTML = content.innerHTML.replace(source, dest); } } }); } } detectColors (avatar) { const images = avatar.querySelectorAll("img"); images.forEach((image) => { image.crossOrigin = "anonymous"; const colorAvatar = () => { const detector = new ColorDetector(image); detector.detect().then((color) => { this.colors.backgroundColor = detector.getHexColor(); this.colors.borderColor = detector.getHexColor({ r: -40, g: -40, b: -40 }); avatar.style.backgroundColor = this.colors.backgroundColor; avatar.style.borderColor = this.colors.borderColor; }); }; if (image.complete) { colorAvatar(); } else { image.onload = colorAvatar; } }); } } class PleromaModCatify extends PleromaMod { constructor () { super("pleroma-mod-catify"); this.animals = {}; this.config = { includes: { css: [ "style.css" ] }, triggers: { cat: { displayName: [ "🐱", "😺", "🐈", "😼", "😹", "にゃ", "cat", "mew", "meow", "nya", "miaou", "kitten", "kitn", "ktn", "kadse", "catte" ], instances: [ "misskey.io" ] }, bear: { displayName: [ "🐻" ], instances: [] }, rabbit: { displayName: [ "🐰", "πŸ‡", "rabbit", "bunny", "hase", "hΓ€schen", "kaninchen" ], instances: [] } }, nya: { enabled: false }, filter: [ "user-info", "timeline", "Conversation", "panel-body", "main", "active", "status-body" ] }; } onSettingInit (key, ul, li) { if (key === "nya") { const label = document.createElement("label"); label.classList.add("checkbox"); const input = document.createElement("input"); input.setAttribute("type", "checkbox"); input.checked = this.config.nya.enabled; input.addEventListener("change", (event) => { if (event.target.checked) { this.config.nya.enabled = true; } else { this.config.nya.enabled = false; } this.saveConfig(); }); label.appendChild(input); const fakeCheckbox = document.createElement("i"); fakeCheckbox.classList.add("checkbox-indicator"); label.appendChild(fakeCheckbox); const text = document.createElement("span"); text.classList.add("label"); text.innerText = "enable nyanification of posts"; label.appendChild(text); li.appendChild(label); ul.appendChild(li); } } onConfigLoad () { return new Promise((resolve) => { this.fetchJson("config.json").then((json) => { resolve(json); }).catch((error) => { console.error("can't parse catify config"); console.error(error); resolve({}); }); }); } onMutation (mutation, observer) { const component = PleromaModLoader.getVueComponent(mutation.target); if (component) { this.isCat(component.user); } mutation.addedNodes.forEach((node) => { const nodeComp = PleromaModLoader.getVueComponent(node); if (nodeComp) { if (nodeComp.user) { this.isCat(nodeComp.user); } if (nodeComp.status) { this.isCat(nodeComp.status.user); } } }); this.detectCats(); for (const element of mutation.addedNodes) { this.catify(element); } } onRun () { this.areYouACat(); this.detectCats(); this.catify(); } onDestroy () { const allCats = document.querySelectorAll(".catified"); for (const cat of allCats) { cat.classList.remove("catified"); for (const type in this.config.triggers) { cat.classList.remove(type); } } } addCat (handle, type) { if (type == null) { type = "cat"; } handle = handle.trim(); if (!this.animals[handle]) { this.animals[handle] = new PleromaCat(handle, type, this.config); } } isCat (user) { if (!user) { return false; } let result = false; Object.keys(this.config.triggers).forEach((type) => { const pattern = this.config.triggers[type].displayName.join("|"); const regex = new RegExp(pattern, "i"); if (regex.test(user.name)) { this.addCat(user.screen_name, type); result = true; return; } if (regex.test(user.screen_name)) { this.addCat(user.screen_name, type); result = true; return; } user.fields.forEach((field) => { if (regex.test(field.name) || regex.test(field.value)) { this.addCat(user.screen_name, type); result = true; } }); }); return result; } areYouACat () { const profile = document.querySelector(".user-card"); const cmp = PleromaModLoader.getVueComponent(profile); if (cmp) { this.isCat(cmp.user); } } detectCatsByClassName (classname) { classname = classname || "status-container"; const hits = document.querySelectorAll("." + classname); hits.forEach((hit) => { if (hit.classList.contains("catified")) { return; } const cmp = PleromaModLoader.getVueComponent(hit); if (cmp) { if (cmp.user) { this.isCat(cmp.user); } if (cmp.status) { this.isCat(cmp.status.user); } } }); } detectCats () { this.detectCatsByClassName("status-container"); this.detectCatsByClassName("basic-user-card"); } catify (element) { for (const catKey in this.animals) { this.animals[catKey].makeCat(element); } } } PleromaModLoader.registerMod(PleromaModCatify);