// ==UserScript== // @name Replace Ugly Avatars // @name:zh-CN 赐你个头像吧 // @namespace https://github.com/utags/replace-ugly-avatars // @homepageURL https://github.com/utags/replace-ugly-avatars#readme // @supportURL https://github.com/utags/replace-ugly-avatars/issues // @version 0.0.7 // @description 🔃 Replace specified user's avatar (profile photo) and username (nickname) // @description:zh-CN 🔃 换掉别人的头像与昵称 // @icon data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%230d6efd' class='bi bi-arrow-repeat' viewBox='0 0 16 16'%3E %3Cpath d='M11.534 7h3.932a.25.25 0 0 1 .192.41l-1.966 2.36a.25.25 0 0 1-.384 0l-1.966-2.36a.25.25 0 0 1 .192-.41zm-11 2h3.932a.25.25 0 0 0 .192-.41L2.692 6.23a.25.25 0 0 0-.384 0L.342 8.59A.25.25 0 0 0 .534 9z'/%3E %3Cpath fill-rule='evenodd' d='M8 3c-1.552 0-2.94.707-3.857 1.818a.5.5 0 1 1-.771-.636A6.002 6.002 0 0 1 13.917 7H12.9A5.002 5.002 0 0 0 8 3zM3.1 9a5.002 5.002 0 0 0 8.757 2.182.5.5 0 1 1 .771.636A6.002 6.002 0 0 1 2.083 9H3.1z'/%3E %3C/svg%3E // @author Pipecraft // @license MIT // @match https://*.v2ex.com/* // @match https://v2hot.pipecraft.net/* // @run-at document-start // @grant GM_addElement // @grant GM.getValue // @grant GM.setValue // @grant GM_addValueChangeListener // @grant GM_removeValueChangeListener // @downloadURL none // ==/UserScript== // ;(() => { "use strict" var doc = document if (typeof String.prototype.replaceAll !== "function") { String.prototype.replaceAll = String.prototype.replace } var $ = (selectors, element) => (element || doc).querySelector(selectors) var $$ = (selectors, element) => [ ...(element || doc).querySelectorAll(selectors), ] var getRootElement = (type) => type === 1 ? doc.head || doc.body || doc.documentElement : type === 2 ? doc.body || doc.documentElement : doc.documentElement var createElement = (tagName, attributes) => setAttributes(doc.createElement(tagName), attributes) var addElement = (parentNode, tagName, attributes) => { if (typeof parentNode === "string") { return addElement(null, parentNode, tagName) } if (!tagName) { return } if (!parentNode) { parentNode = /^(script|link|style|meta)$/.test(tagName) ? getRootElement(1) : getRootElement(2) } if (typeof tagName === "string") { const element = createElement(tagName, attributes) parentNode.append(element) return element } setAttributes(tagName, attributes) parentNode.append(tagName) return tagName } var addEventListener = (element, type, listener, options) => { if (!element) { return } if (typeof type === "object") { for (const type1 in type) { if (Object.hasOwn(type, type1)) { element.addEventListener(type1, type[type1]) } } } else if (typeof type === "string" && typeof listener === "function") { element.addEventListener(type, listener, options) } } var removeEventListener = (element, type, listener, options) => { if (!element) { return } if (typeof type === "object") { for (const type1 in type) { if (Object.hasOwn(type, type1)) { element.removeEventListener(type1, type[type1]) } } } else if (typeof type === "string" && typeof listener === "function") { element.removeEventListener(type, listener, options) } } var setAttribute = (element, name, value) => element ? element.setAttribute(name, value) : void 0 var setAttributes = (element, attributes) => { if (element && attributes) { for (const name in attributes) { if (Object.hasOwn(attributes, name)) { const value = attributes[name] if (value === void 0) { continue } if (/^(value|textContent|innerText)$/.test(name)) { element[name] = value } else if (/^(innerHTML)$/.test(name)) { element[name] = createHTML(value) } else if (name === "style") { setStyle(element, value, true) } else if (/on\w+/.test(name)) { const type = name.slice(2) addEventListener(element, type, value) } else { setAttribute(element, name, value) } } } } return element } var addClass = (element, className) => { if (!element || !element.classList) { return } element.classList.add(className) } var removeClass = (element, className) => { if (!element || !element.classList) { return } element.classList.remove(className) } var setStyle = (element, values, overwrite) => { if (!element) { return } const style = element.style if (typeof values === "string") { style.cssText = overwrite ? values : style.cssText + ";" + values return } if (overwrite) { style.cssText = "" } for (const key in values) { if (Object.hasOwn(values, key)) { style[key] = values[key].replace("!important", "") } } } var throttle = (func, interval) => { let timeoutId = null let next = false const handler = (...args) => { if (timeoutId) { next = true } else { func.apply(void 0, args) timeoutId = setTimeout(() => { timeoutId = null if (next) { next = false handler() } }, interval) } } return handler } if (typeof Object.hasOwn !== "function") { Object.hasOwn = (instance, prop) => Object.prototype.hasOwnProperty.call(instance, prop) } var getOffsetPosition = (element, referElement) => { const position = { top: 0, left: 0 } referElement = referElement || doc.body while (element && element !== referElement) { position.top += element.offsetTop position.left += element.offsetLeft element = element.offsetParent } return position } var headFuncArray = [] var bodyFuncArray = [] var headBodyObserver var startObserveHeadBodyExists = () => { if (headBodyObserver) { return } headBodyObserver = new MutationObserver(() => { if (doc.head && doc.body) { headBodyObserver.disconnect() } if (doc.head && headFuncArray.length > 0) { for (const func of headFuncArray) { func() } headFuncArray.length = 0 } if (doc.body && bodyFuncArray.length > 0) { for (const func of bodyFuncArray) { func() } bodyFuncArray.length = 0 } }) headBodyObserver.observe(doc, { childList: true, subtree: true, }) } var runWhenHeadExists = (func) => { if (!doc.head) { headFuncArray.push(func) startObserveHeadBodyExists() return } func() } var escapeHTMLPolicy = typeof trustedTypes !== "undefined" && typeof trustedTypes.createPolicy === "function" ? trustedTypes.createPolicy("beuEscapePolicy", { createHTML: (string) => string, }) : void 0 var createHTML = (html) => { return escapeHTMLPolicy ? escapeHTMLPolicy.createHTML(html) : html } var addElement2 = typeof GM_addElement === "function" ? (parentNode, tagName, attributes) => { if (typeof parentNode === "string") { return addElement2(null, parentNode, tagName) } if (!tagName) { return } if (!parentNode) { parentNode = /^(script|link|style|meta)$/.test(tagName) ? getRootElement(1) : getRootElement(2) } if (typeof tagName === "string") { let attributes2 if (attributes) { const entries1 = [] const entries2 = [] for (const entry of Object.entries(attributes)) { if (/^(on\w+|innerHTML)$/.test(entry[0])) { entries2.push(entry) } else { entries1.push(entry) } } attributes = Object.fromEntries(entries1) attributes2 = Object.fromEntries(entries2) } const element = GM_addElement(null, tagName, attributes) setAttributes(element, attributes2) parentNode.append(element) return element } setAttributes(tagName, attributes) parentNode.append(tagName) return tagName } : addElement var content_default = '#rua_container .change_button{position:absolute;box-sizing:border-box;width:20px;height:20px;padding:1px;border:1px solid;cursor:pointer;color:#0d6efd}#rua_container .change_button.advanced{color:#00008b;display:none}#rua_container .change_button.hide{display:none}#rua_container .change_button:active,#rua_container .change_button.active{opacity:50%;transition:all .2s}#rua_container:hover .change_button{display:block}img.rua_fadeout{box-sizing:border-box;padding:45%;transition:all 1s ease-out}#Main .header .fr a img{width:73px;height:73px}td[width="48"] img{width:48px;height:48px}' var styles = [ "adventurer", "adventurer-neutral", "avataaars", "avataaars-neutral", "big-ears", "big-ears-neutral", "big-smile", "bottts", "bottts-neutral", "croodles", "croodles-neutral", "fun-emoji", "icons", "identicon", "initials", "lorelei", "lorelei-neutral", "micah", "miniavs", "notionists", "notionists-neutral", "open-peeps", "personas", "pixel-art", "pixel-art-neutral", "shapes", "thumbs", ] function getRandomInt(min, max) { min = Math.ceil(min) max = Math.floor(max) return Math.floor(Math.random() * (max - min)) + min } function getRandomFlipParameter(style) { if (style === "initials" || style === "identicon") { return "" } const values = [false, false, false, false, true] const value = values[getRandomInt(0, values.length)] return value ? "&flip=true" : "" } function getRandomRadiusParameter(style) { const values = [0, 0, 0, 10, 10, 10, 20, 20, 30, 50] const value = values[getRandomInt(0, values.length)] return value ? "&radius=" + value : "" } function getRandomBackgroundColorParameter(style) { const values = [ "", "", "", "", "", "", "", "", "", "ffffff", "b6e3f4", "c0aede", "d1d4f9", "ffd5dc", "ffdfbf", ] const value = values[getRandomInt(0, values.length)] return value ? "&backgroundColor=" + value : "" } function getRandomAvatar(prefix) { const randomStyle = styles[getRandomInt(0, styles.length)] return ( "https://api.dicebear.com/6.x/" .concat(randomStyle, "/svg?seed=") .concat(prefix, ".") .concat(Date.now()) + getRandomFlipParameter(randomStyle) + getRandomRadiusParameter(randomStyle) + getRandomBackgroundColorParameter(randomStyle) ) } var changeIcon = '\n\n\n' var listeners = {} var getValue = async (key) => { const value = await GM.getValue(key) return value && value !== "undefined" ? JSON.parse(value) : void 0 } var setValue = async (key, value) => { if (value !== void 0) { const newValue = JSON.stringify(value) if (listeners[key]) { const oldValue = await GM.getValue(key) await GM.setValue(key, newValue) if (newValue !== oldValue) { for (const func of listeners[key]) { func(key, oldValue, newValue) } } } else { await GM.setValue(key, newValue) } } } var _addValueChangeListener = (key, func) => { listeners[key] = listeners[key] || [] listeners[key].push(func) return () => { if (listeners[key] && listeners[key].length > 0) { for (let i = listeners[key].length - 1; i >= 0; i--) { if (listeners[key][i] === func) { listeners[key].splice(i, 1) } } } } } var addValueChangeListener = (key, func) => { if (typeof GM_addValueChangeListener !== "function") { console.warn("Do not support GM_addValueChangeListener!") return _addValueChangeListener(key, func) } const listenerId = GM_addValueChangeListener(key, func) return () => { GM_removeValueChangeListener(listenerId) } } var host = location.host var storageKey = "avatar:v2ex.com" async function saveAvatar(userName, src) { const values = (await getValue(storageKey)) || {} values[userName] = src await setValue(storageKey, values) } var cachedValues = {} async function reloadCachedValues() { cachedValues = (await getValue(storageKey)) || {} } function getChangedAavatar(userName) { return cachedValues[userName] } async function initStorage(options) { addValueChangeListener(storageKey, async () => { await reloadCachedValues() if (options && typeof options.avatarValueChangeListener === "function") { options.avatarValueChangeListener() } }) await reloadCachedValues() } function isAvatar(element) { if (!element || element.tagName !== "IMG") { return false } if (element.dataset.ruaUserName) { return true } return false } var currentTarget function addChangeButton(element) { currentTarget = element const container = $("#rua_container") || addElement2(doc.body, "div", { id: "rua_container", }) const changeButton = $(".change_button.quick", container) || addElement2(container, "button", { innerHTML: changeIcon, class: "change_button quick", async onclick() { addClass(changeButton, "active") setTimeout(() => { removeClass(changeButton, "active") }, 200) const userName = currentTarget.dataset.ruaUserName || "noname" const avatarUrl = getRandomAvatar(userName) changeAvatar(currentTarget, avatarUrl, true) await saveAvatar(userName, avatarUrl) }, }) const changeButton2 = $(".change_button.advanced", container) || addElement2(container, "button", { innerHTML: changeIcon, class: "change_button advanced", async onclick() { addClass(changeButton2, "active") setTimeout(() => { removeClass(changeButton2, "active") }, 200) const userName = currentTarget.dataset.ruaUserName || "noname" const avatarUrl = prompt( "\u8BF7\u8F93\u5165\u5934\u50CF\u94FE\u63A5", "" ) if (avatarUrl) { changeAvatar(currentTarget, avatarUrl, true) await saveAvatar(userName, avatarUrl) } }, }) removeClass(changeButton, "hide") removeClass(changeButton2, "hide") const pos = getOffsetPosition(element) const leftOffset = element.clientWidth - changeButton.clientWidth > 20 ? element.clientWidth - changeButton.clientWidth : element.clientWidth - 1 changeButton.style.top = pos.top + "px" changeButton.style.left = pos.left + leftOffset + "px" changeButton2.style.top = pos.top + changeButton.clientHeight + "px" changeButton2.style.left = pos.left + leftOffset + "px" const mouseoutHandler = () => { addClass(changeButton, "hide") addClass(changeButton2, "hide") removeEventListener(element, "mouseout", mouseoutHandler) } addEventListener(element, "mouseout", mouseoutHandler) } function getUserName(element) { if (!element) { return } const userNameElement = $('a[href*="/member/"]', element) if (userNameElement) { const userName = (/member\/(\w+)/.exec(userNameElement.href) || [])[1] if (userName) { return userName.toLowerCase() } return } return getUserName(element.parentElement) } function changeAvatar(element, src, animation = false) { if (element.ruaLoading) { return } if (!element.dataset.orgSrc) { const orgSrc = element.dataset.src || element.src element.dataset.orgSrc = orgSrc } element.ruaLoading = true const imgOnloadHandler = () => { element.ruaLoading = false removeClass(element, "rua_fadeout") removeEventListener(element, "load", imgOnloadHandler) removeEventListener(element, "error", imgOnloadHandler) } addEventListener(element, "load", imgOnloadHandler) addEventListener(element, "error", imgOnloadHandler) const width = element.clientWidth const height = element.clientHeight if (width > 1) { element.style.width = width + "px" } if (height > 1) { element.style.height = height + "px" } if (animation) { addClass(element, "rua_fadeout") } else { element.src = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" } setTimeout(() => { element.src = src }) if (element.dataset.src) { element.dataset.src = src } } function scanAvatars() { const avatars = $$('.avatar,a[href*="/member/"] img') for (const avatar of avatars) { let userName = avatar.dataset.ruaUserName if (!userName) { userName = getUserName(avatar) if (!userName) { console.error("Can't get username", avatar, userName) continue } avatar.dataset.ruaUserName = userName } const newAvatarSrc = getChangedAavatar(userName) if (newAvatarSrc && avatar.src !== newAvatarSrc) { changeAvatar(avatar, newAvatarSrc) } } } async function main() { if ($("#rua_tyle")) { return } runWhenHeadExists(() => { addElement2("style", { textContent: content_default, id: "rua_tyle", }) }) addEventListener(doc, "mouseover", (event) => { const target = event.target if (!isAvatar(target)) { return } addChangeButton(target) }) await initStorage({ avatarValueChangeListener() { scanAvatars() }, }) scanAvatars() const observer = new MutationObserver( throttle(async () => { scanAvatars() }, 500) ) observer.observe(doc, { childList: true, subtree: true, }) } main() })()