赐你个头像吧

换掉别人的头像与昵称

当前为 2023-08-07 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Replace Ugly Avatars
  3. // @name:zh-CN 赐你个头像吧
  4. // @namespace https://github.com/utags/replace-ugly-avatars
  5. // @homepageURL https://github.com/utags/replace-ugly-avatars#readme
  6. // @supportURL https://github.com/utags/replace-ugly-avatars/issues
  7. // @version 0.0.1
  8. // @description Replace specified user's avatar (profile photo) and username (nickname)
  9. // @description:zh-CN 换掉别人的头像与昵称
  10. // @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
  11. // @author Pipecraft
  12. // @license MIT
  13. // @match https://*.v2ex.com/*
  14. // @match https://v2hot.pipecraft.net/*
  15. // @run-at document-start
  16. // @grant GM_addElement
  17. // @grant GM.getValue
  18. // @grant GM.setValue
  19. // @grant GM_addValueChangeListener
  20. // @grant GM_removeValueChangeListener
  21. // ==/UserScript==
  22. //
  23. ;(() => {
  24. "use strict"
  25. var doc = document
  26. if (typeof String.prototype.replaceAll !== "function") {
  27. String.prototype.replaceAll = String.prototype.replace
  28. }
  29. var $ = (selectors, element) => (element || doc).querySelector(selectors)
  30. var $$ = (selectors, element) => [
  31. ...(element || doc).querySelectorAll(selectors),
  32. ]
  33. var getRootElement = (type) =>
  34. type === 1
  35. ? doc.head || doc.body || doc.documentElement
  36. : type === 2
  37. ? doc.body || doc.documentElement
  38. : doc.documentElement
  39. var createElement = (tagName, attributes) =>
  40. setAttributes(doc.createElement(tagName), attributes)
  41. var addElement = (parentNode, tagName, attributes) => {
  42. if (typeof parentNode === "string") {
  43. return addElement(null, parentNode, tagName)
  44. }
  45. if (!tagName) {
  46. return
  47. }
  48. if (!parentNode) {
  49. parentNode = /^(script|link|style|meta)$/.test(tagName)
  50. ? getRootElement(1)
  51. : getRootElement(2)
  52. }
  53. if (typeof tagName === "string") {
  54. const element = createElement(tagName, attributes)
  55. parentNode.append(element)
  56. return element
  57. }
  58. setAttributes(tagName, attributes)
  59. parentNode.append(tagName)
  60. return tagName
  61. }
  62. var addEventListener = (element, type, listener, options) => {
  63. if (!element) {
  64. return
  65. }
  66. if (typeof type === "object") {
  67. for (const type1 in type) {
  68. if (Object.hasOwn(type, type1)) {
  69. element.addEventListener(type1, type[type1])
  70. }
  71. }
  72. } else if (typeof type === "string" && typeof listener === "function") {
  73. element.addEventListener(type, listener, options)
  74. }
  75. }
  76. var removeEventListener = (element, type, listener, options) => {
  77. if (!element) {
  78. return
  79. }
  80. if (typeof type === "object") {
  81. for (const type1 in type) {
  82. if (Object.hasOwn(type, type1)) {
  83. element.removeEventListener(type1, type[type1])
  84. }
  85. }
  86. } else if (typeof type === "string" && typeof listener === "function") {
  87. element.removeEventListener(type, listener, options)
  88. }
  89. }
  90. var setAttribute = (element, name, value) =>
  91. element ? element.setAttribute(name, value) : void 0
  92. var setAttributes = (element, attributes) => {
  93. if (element && attributes) {
  94. for (const name in attributes) {
  95. if (Object.hasOwn(attributes, name)) {
  96. const value = attributes[name]
  97. if (value === void 0) {
  98. continue
  99. }
  100. if (/^(value|textContent|innerText)$/.test(name)) {
  101. element[name] = value
  102. } else if (/^(innerHTML)$/.test(name)) {
  103. element[name] = createHTML(value)
  104. } else if (name === "style") {
  105. setStyle(element, value, true)
  106. } else if (/on\w+/.test(name)) {
  107. const type = name.slice(2)
  108. addEventListener(element, type, value)
  109. } else {
  110. setAttribute(element, name, value)
  111. }
  112. }
  113. }
  114. }
  115. return element
  116. }
  117. var addClass = (element, className) => {
  118. if (!element || !element.classList) {
  119. return
  120. }
  121. element.classList.add(className)
  122. }
  123. var removeClass = (element, className) => {
  124. if (!element || !element.classList) {
  125. return
  126. }
  127. element.classList.remove(className)
  128. }
  129. var setStyle = (element, values, overwrite) => {
  130. if (!element) {
  131. return
  132. }
  133. const style = element.style
  134. if (typeof values === "string") {
  135. style.cssText = overwrite ? values : style.cssText + ";" + values
  136. return
  137. }
  138. if (overwrite) {
  139. style.cssText = ""
  140. }
  141. for (const key in values) {
  142. if (Object.hasOwn(values, key)) {
  143. style[key] = values[key].replace("!important", "")
  144. }
  145. }
  146. }
  147. var throttle = (func, interval) => {
  148. let timeoutId = null
  149. let next = false
  150. const handler = (...args) => {
  151. if (timeoutId) {
  152. next = true
  153. } else {
  154. func.apply(void 0, args)
  155. timeoutId = setTimeout(() => {
  156. timeoutId = null
  157. if (next) {
  158. next = false
  159. handler()
  160. }
  161. }, interval)
  162. }
  163. }
  164. return handler
  165. }
  166. if (typeof Object.hasOwn !== "function") {
  167. Object.hasOwn = (instance, prop) =>
  168. Object.prototype.hasOwnProperty.call(instance, prop)
  169. }
  170. var getOffsetPosition = (element, referElement) => {
  171. const position = { top: 0, left: 0 }
  172. referElement = referElement || doc.body
  173. while (element && element !== referElement) {
  174. position.top += element.offsetTop
  175. position.left += element.offsetLeft
  176. element = element.offsetParent
  177. }
  178. return position
  179. }
  180. var escapeHTMLPolicy =
  181. typeof trustedTypes !== "undefined" &&
  182. typeof trustedTypes.createPolicy === "function"
  183. ? trustedTypes.createPolicy("beuEscapePolicy", {
  184. createHTML: (string) => string,
  185. })
  186. : void 0
  187. var createHTML = (html) => {
  188. return escapeHTMLPolicy ? escapeHTMLPolicy.createHTML(html) : html
  189. }
  190. var addElement2 =
  191. typeof GM_addElement === "function"
  192. ? (parentNode, tagName, attributes) => {
  193. if (typeof parentNode === "string") {
  194. return addElement2(null, parentNode, tagName)
  195. }
  196. if (!tagName) {
  197. return
  198. }
  199. if (!parentNode) {
  200. parentNode = /^(script|link|style|meta)$/.test(tagName)
  201. ? getRootElement(1)
  202. : getRootElement(2)
  203. }
  204. if (typeof tagName === "string") {
  205. let attributes2
  206. if (attributes) {
  207. const entries1 = []
  208. const entries2 = []
  209. for (const entry of Object.entries(attributes)) {
  210. if (/^(on\w+|innerHTML)$/.test(entry[0])) {
  211. entries2.push(entry)
  212. } else {
  213. entries1.push(entry)
  214. }
  215. }
  216. attributes = Object.fromEntries(entries1)
  217. attributes2 = Object.fromEntries(entries2)
  218. }
  219. const element = GM_addElement(null, tagName, attributes)
  220. setAttributes(element, attributes2)
  221. parentNode.append(element)
  222. return element
  223. }
  224. setAttributes(tagName, attributes)
  225. parentNode.append(tagName)
  226. return tagName
  227. }
  228. : addElement
  229. var content_default =
  230. "#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}#Main .header .fr a img{width:73px;height:73px}"
  231. var styles = [
  232. "adventurer",
  233. "adventurer-neutral",
  234. "avataaars",
  235. "avataaars-neutral",
  236. "big-ears",
  237. "big-ears-neutral",
  238. "big-smile",
  239. "bottts",
  240. "bottts-neutral",
  241. "croodles",
  242. "croodles-neutral",
  243. "fun-emoji",
  244. "icons",
  245. "identicon",
  246. "initials",
  247. "lorelei",
  248. "lorelei-neutral",
  249. "micah",
  250. "miniavs",
  251. "notionists",
  252. "notionists-neutral",
  253. "open-peeps",
  254. "personas",
  255. "pixel-art",
  256. "pixel-art-neutral",
  257. "shapes",
  258. "thumbs",
  259. ]
  260. function getRandomInt(min, max) {
  261. min = Math.ceil(min)
  262. max = Math.floor(max)
  263. return Math.floor(Math.random() * (max - min)) + min
  264. }
  265. function getRandomAvatar(prefix) {
  266. const randomStyle = styles[getRandomInt(0, styles.length)]
  267. return "https://api.dicebear.com/6.x/"
  268. .concat(randomStyle, "/svg?seed=")
  269. .concat(prefix, ".")
  270. .concat(Date.now())
  271. }
  272. var changeIcon =
  273. '<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>'
  274. var listeners = {}
  275. var getValue = async (key) => {
  276. const value = await GM.getValue(key)
  277. return value && value !== "undefined" ? JSON.parse(value) : void 0
  278. }
  279. var setValue = async (key, value) => {
  280. if (value !== void 0) {
  281. const newValue = JSON.stringify(value)
  282. if (listeners[key]) {
  283. const oldValue = await GM.getValue(key)
  284. await GM.setValue(key, newValue)
  285. if (newValue !== oldValue) {
  286. for (const func of listeners[key]) {
  287. func(key, oldValue, newValue)
  288. }
  289. }
  290. } else {
  291. await GM.setValue(key, newValue)
  292. }
  293. }
  294. }
  295. var _addValueChangeListener = (key, func) => {
  296. listeners[key] = listeners[key] || []
  297. listeners[key].push(func)
  298. return () => {
  299. if (listeners[key] && listeners[key].length > 0) {
  300. for (let i = listeners[key].length - 1; i >= 0; i--) {
  301. if (listeners[key][i] === func) {
  302. listeners[key].splice(i, 1)
  303. }
  304. }
  305. }
  306. }
  307. }
  308. var addValueChangeListener = (key, func) => {
  309. if (typeof GM_addValueChangeListener !== "function") {
  310. console.warn("Do not support GM_addValueChangeListener!")
  311. return _addValueChangeListener(key, func)
  312. }
  313. const listenerId = GM_addValueChangeListener(key, func)
  314. return () => {
  315. GM_removeValueChangeListener(listenerId)
  316. }
  317. }
  318. var host = location.host
  319. var storageKey = "avatar:v2ex.com"
  320. async function saveAvatar(userName, src) {
  321. const values = (await getValue(storageKey)) || {}
  322. values[userName] = src
  323. await setValue(storageKey, values)
  324. }
  325. var cachedValues = {}
  326. async function reloadCachedValues() {
  327. cachedValues = (await getValue(storageKey)) || {}
  328. }
  329. function getChangedAavatar(userName) {
  330. return cachedValues[userName]
  331. }
  332. async function initStorage(options) {
  333. addValueChangeListener(storageKey, async () => {
  334. await reloadCachedValues()
  335. if (options && typeof options.avatarValueChangeListener === "function") {
  336. options.avatarValueChangeListener()
  337. }
  338. })
  339. await reloadCachedValues()
  340. }
  341. function isAvatar(element) {
  342. if (!element || element.tagName !== "IMG") {
  343. return false
  344. }
  345. if (element.dataset.ruaUserName) {
  346. return true
  347. }
  348. return false
  349. }
  350. var currentTarget
  351. function addChangeButton(element) {
  352. currentTarget = element
  353. const container =
  354. $("#rua_container") ||
  355. addElement2(doc.body, "div", {
  356. id: "rua_container",
  357. })
  358. const changeButton =
  359. $(".change_button.quick", container) ||
  360. addElement2(container, "button", {
  361. innerHTML: changeIcon,
  362. class: "change_button quick",
  363. async onclick() {
  364. addClass(changeButton, "active")
  365. setTimeout(() => {
  366. removeClass(changeButton, "active")
  367. }, 200)
  368. const userName = currentTarget.dataset.ruaUserName || "noname"
  369. const avatarUrl = getRandomAvatar(userName)
  370. changeAvatar(currentTarget, avatarUrl)
  371. await saveAvatar(userName, avatarUrl)
  372. },
  373. })
  374. const changeButton2 =
  375. $(".change_button.advanced", container) ||
  376. addElement2(container, "button", {
  377. innerHTML: changeIcon,
  378. class: "change_button advanced",
  379. async onclick() {
  380. addClass(changeButton2, "active")
  381. setTimeout(() => {
  382. removeClass(changeButton2, "active")
  383. }, 200)
  384. const userName = currentTarget.dataset.ruaUserName || "noname"
  385. const avatarUrl = prompt(
  386. "\u8BF7\u8F93\u5165\u5934\u50CF\u94FE\u63A5",
  387. ""
  388. )
  389. if (avatarUrl) {
  390. changeAvatar(currentTarget, avatarUrl)
  391. await saveAvatar(userName, avatarUrl)
  392. }
  393. },
  394. })
  395. removeClass(changeButton, "hide")
  396. removeClass(changeButton2, "hide")
  397. const pos = getOffsetPosition(element)
  398. changeButton.style.top = pos.top + "px"
  399. changeButton.style.left =
  400. pos.left + element.clientWidth - changeButton.clientWidth + "px"
  401. changeButton2.style.top = pos.top + changeButton.clientHeight + "px"
  402. changeButton2.style.left =
  403. pos.left + element.clientWidth - changeButton.clientWidth + "px"
  404. const mouseoutHandler = () => {
  405. addClass(changeButton, "hide")
  406. addClass(changeButton2, "hide")
  407. removeEventListener(element, "mouseout", mouseoutHandler)
  408. }
  409. addEventListener(element, "mouseout", mouseoutHandler)
  410. }
  411. function getUserName(element) {
  412. if (!element) {
  413. return
  414. }
  415. const userNameElement = $('a[href*="/member/"]', element)
  416. if (userNameElement) {
  417. return (/member\/(\w+)/.exec(userNameElement.href) || [])[1]
  418. }
  419. return getUserName(element.parentElement)
  420. }
  421. function changeAvatar(element, src) {
  422. if (!element.dataset.orgSrc) {
  423. const orgSrc = element.dataset.src || element.src
  424. element.dataset.orgSrc = orgSrc
  425. }
  426. element.src = src
  427. if (element.dataset.src) {
  428. element.dataset.src = src
  429. }
  430. }
  431. function scanAvatars() {
  432. const avatars = $$('.avatar,a[href*="/member/"] img')
  433. for (const avatar of avatars) {
  434. let userName = avatar.dataset.ruaUserName
  435. if (!userName) {
  436. userName = getUserName(avatar)
  437. if (!userName) {
  438. console.error("Can't get username", avatar, userName)
  439. continue
  440. }
  441. avatar.dataset.ruaUserName = userName
  442. }
  443. const newAvatarSrc = getChangedAavatar(userName)
  444. if (newAvatarSrc && avatar.src !== newAvatarSrc) {
  445. changeAvatar(avatar, newAvatarSrc)
  446. }
  447. }
  448. }
  449. async function main() {
  450. if ($("#rua_tyle")) {
  451. return
  452. }
  453. addElement2("style", {
  454. textContent: content_default,
  455. id: "rua_tyle",
  456. })
  457. addEventListener(doc, "mouseover", (event) => {
  458. const target = event.target
  459. if (!isAvatar(target)) {
  460. return
  461. }
  462. addChangeButton(target)
  463. })
  464. await initStorage({
  465. avatarValueChangeListener() {
  466. scanAvatars()
  467. },
  468. })
  469. scanAvatars()
  470. const observer = new MutationObserver(
  471. throttle(async () => {
  472. scanAvatars()
  473. }, 500)
  474. )
  475. observer.observe(doc, {
  476. childList: true,
  477. subtree: true,
  478. })
  479. }
  480. main()
  481. })()