// ==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.5
// @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
// ==/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:20px;transition:all 2s 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 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())
}
var changeIcon =
'<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-repeat" viewBox="0 0 16 16">\n<path 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"/>\n<path 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"/>\n</svg>'
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)
changeButton.style.top = pos.top + "px"
changeButton.style.left =
pos.left + element.clientWidth - changeButton.clientWidth + "px"
changeButton2.style.top = pos.top + changeButton.clientHeight + "px"
changeButton2.style.left =
pos.left + element.clientWidth - changeButton.clientWidth + "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()
})()