赐你个头像吧

🔃 换掉别人的头像与昵称

目前为 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.5
  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 headFuncArray = []
  181. var bodyFuncArray = []
  182. var headBodyObserver
  183. var startObserveHeadBodyExists = () => {
  184. if (headBodyObserver) {
  185. return
  186. }
  187. headBodyObserver = new MutationObserver(() => {
  188. if (doc.head && doc.body) {
  189. headBodyObserver.disconnect()
  190. }
  191. if (doc.head && headFuncArray.length > 0) {
  192. for (const func of headFuncArray) {
  193. func()
  194. }
  195. headFuncArray.length = 0
  196. }
  197. if (doc.body && bodyFuncArray.length > 0) {
  198. for (const func of bodyFuncArray) {
  199. func()
  200. }
  201. bodyFuncArray.length = 0
  202. }
  203. })
  204. headBodyObserver.observe(doc, {
  205. childList: true,
  206. subtree: true,
  207. })
  208. }
  209. var runWhenHeadExists = (func) => {
  210. if (!doc.head) {
  211. headFuncArray.push(func)
  212. startObserveHeadBodyExists()
  213. return
  214. }
  215. func()
  216. }
  217. var escapeHTMLPolicy =
  218. typeof trustedTypes !== "undefined" &&
  219. typeof trustedTypes.createPolicy === "function"
  220. ? trustedTypes.createPolicy("beuEscapePolicy", {
  221. createHTML: (string) => string,
  222. })
  223. : void 0
  224. var createHTML = (html) => {
  225. return escapeHTMLPolicy ? escapeHTMLPolicy.createHTML(html) : html
  226. }
  227. var addElement2 =
  228. typeof GM_addElement === "function"
  229. ? (parentNode, tagName, attributes) => {
  230. if (typeof parentNode === "string") {
  231. return addElement2(null, parentNode, tagName)
  232. }
  233. if (!tagName) {
  234. return
  235. }
  236. if (!parentNode) {
  237. parentNode = /^(script|link|style|meta)$/.test(tagName)
  238. ? getRootElement(1)
  239. : getRootElement(2)
  240. }
  241. if (typeof tagName === "string") {
  242. let attributes2
  243. if (attributes) {
  244. const entries1 = []
  245. const entries2 = []
  246. for (const entry of Object.entries(attributes)) {
  247. if (/^(on\w+|innerHTML)$/.test(entry[0])) {
  248. entries2.push(entry)
  249. } else {
  250. entries1.push(entry)
  251. }
  252. }
  253. attributes = Object.fromEntries(entries1)
  254. attributes2 = Object.fromEntries(entries2)
  255. }
  256. const element = GM_addElement(null, tagName, attributes)
  257. setAttributes(element, attributes2)
  258. parentNode.append(element)
  259. return element
  260. }
  261. setAttributes(tagName, attributes)
  262. parentNode.append(tagName)
  263. return tagName
  264. }
  265. : addElement
  266. var content_default =
  267. '#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}'
  268. var styles = [
  269. "adventurer",
  270. "adventurer-neutral",
  271. "avataaars",
  272. "avataaars-neutral",
  273. "big-ears",
  274. "big-ears-neutral",
  275. "big-smile",
  276. "bottts",
  277. "bottts-neutral",
  278. "croodles",
  279. "croodles-neutral",
  280. "fun-emoji",
  281. "icons",
  282. "identicon",
  283. "initials",
  284. "lorelei",
  285. "lorelei-neutral",
  286. "micah",
  287. "miniavs",
  288. "notionists",
  289. "notionists-neutral",
  290. "open-peeps",
  291. "personas",
  292. "pixel-art",
  293. "pixel-art-neutral",
  294. "shapes",
  295. "thumbs",
  296. ]
  297. function getRandomInt(min, max) {
  298. min = Math.ceil(min)
  299. max = Math.floor(max)
  300. return Math.floor(Math.random() * (max - min)) + min
  301. }
  302. function getRandomAvatar(prefix) {
  303. const randomStyle = styles[getRandomInt(0, styles.length)]
  304. return "https://api.dicebear.com/6.x/"
  305. .concat(randomStyle, "/svg?seed=")
  306. .concat(prefix, ".")
  307. .concat(Date.now())
  308. }
  309. var changeIcon =
  310. '<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>'
  311. var listeners = {}
  312. var getValue = async (key) => {
  313. const value = await GM.getValue(key)
  314. return value && value !== "undefined" ? JSON.parse(value) : void 0
  315. }
  316. var setValue = async (key, value) => {
  317. if (value !== void 0) {
  318. const newValue = JSON.stringify(value)
  319. if (listeners[key]) {
  320. const oldValue = await GM.getValue(key)
  321. await GM.setValue(key, newValue)
  322. if (newValue !== oldValue) {
  323. for (const func of listeners[key]) {
  324. func(key, oldValue, newValue)
  325. }
  326. }
  327. } else {
  328. await GM.setValue(key, newValue)
  329. }
  330. }
  331. }
  332. var _addValueChangeListener = (key, func) => {
  333. listeners[key] = listeners[key] || []
  334. listeners[key].push(func)
  335. return () => {
  336. if (listeners[key] && listeners[key].length > 0) {
  337. for (let i = listeners[key].length - 1; i >= 0; i--) {
  338. if (listeners[key][i] === func) {
  339. listeners[key].splice(i, 1)
  340. }
  341. }
  342. }
  343. }
  344. }
  345. var addValueChangeListener = (key, func) => {
  346. if (typeof GM_addValueChangeListener !== "function") {
  347. console.warn("Do not support GM_addValueChangeListener!")
  348. return _addValueChangeListener(key, func)
  349. }
  350. const listenerId = GM_addValueChangeListener(key, func)
  351. return () => {
  352. GM_removeValueChangeListener(listenerId)
  353. }
  354. }
  355. var host = location.host
  356. var storageKey = "avatar:v2ex.com"
  357. async function saveAvatar(userName, src) {
  358. const values = (await getValue(storageKey)) || {}
  359. values[userName] = src
  360. await setValue(storageKey, values)
  361. }
  362. var cachedValues = {}
  363. async function reloadCachedValues() {
  364. cachedValues = (await getValue(storageKey)) || {}
  365. }
  366. function getChangedAavatar(userName) {
  367. return cachedValues[userName]
  368. }
  369. async function initStorage(options) {
  370. addValueChangeListener(storageKey, async () => {
  371. await reloadCachedValues()
  372. if (options && typeof options.avatarValueChangeListener === "function") {
  373. options.avatarValueChangeListener()
  374. }
  375. })
  376. await reloadCachedValues()
  377. }
  378. function isAvatar(element) {
  379. if (!element || element.tagName !== "IMG") {
  380. return false
  381. }
  382. if (element.dataset.ruaUserName) {
  383. return true
  384. }
  385. return false
  386. }
  387. var currentTarget
  388. function addChangeButton(element) {
  389. currentTarget = element
  390. const container =
  391. $("#rua_container") ||
  392. addElement2(doc.body, "div", {
  393. id: "rua_container",
  394. })
  395. const changeButton =
  396. $(".change_button.quick", container) ||
  397. addElement2(container, "button", {
  398. innerHTML: changeIcon,
  399. class: "change_button quick",
  400. async onclick() {
  401. addClass(changeButton, "active")
  402. setTimeout(() => {
  403. removeClass(changeButton, "active")
  404. }, 200)
  405. const userName = currentTarget.dataset.ruaUserName || "noname"
  406. const avatarUrl = getRandomAvatar(userName)
  407. changeAvatar(currentTarget, avatarUrl, true)
  408. await saveAvatar(userName, avatarUrl)
  409. },
  410. })
  411. const changeButton2 =
  412. $(".change_button.advanced", container) ||
  413. addElement2(container, "button", {
  414. innerHTML: changeIcon,
  415. class: "change_button advanced",
  416. async onclick() {
  417. addClass(changeButton2, "active")
  418. setTimeout(() => {
  419. removeClass(changeButton2, "active")
  420. }, 200)
  421. const userName = currentTarget.dataset.ruaUserName || "noname"
  422. const avatarUrl = prompt(
  423. "\u8BF7\u8F93\u5165\u5934\u50CF\u94FE\u63A5",
  424. ""
  425. )
  426. if (avatarUrl) {
  427. changeAvatar(currentTarget, avatarUrl, true)
  428. await saveAvatar(userName, avatarUrl)
  429. }
  430. },
  431. })
  432. removeClass(changeButton, "hide")
  433. removeClass(changeButton2, "hide")
  434. const pos = getOffsetPosition(element)
  435. changeButton.style.top = pos.top + "px"
  436. changeButton.style.left =
  437. pos.left + element.clientWidth - changeButton.clientWidth + "px"
  438. changeButton2.style.top = pos.top + changeButton.clientHeight + "px"
  439. changeButton2.style.left =
  440. pos.left + element.clientWidth - changeButton.clientWidth + "px"
  441. const mouseoutHandler = () => {
  442. addClass(changeButton, "hide")
  443. addClass(changeButton2, "hide")
  444. removeEventListener(element, "mouseout", mouseoutHandler)
  445. }
  446. addEventListener(element, "mouseout", mouseoutHandler)
  447. }
  448. function getUserName(element) {
  449. if (!element) {
  450. return
  451. }
  452. const userNameElement = $('a[href*="/member/"]', element)
  453. if (userNameElement) {
  454. const userName = (/member\/(\w+)/.exec(userNameElement.href) || [])[1]
  455. if (userName) {
  456. return userName.toLowerCase()
  457. }
  458. return
  459. }
  460. return getUserName(element.parentElement)
  461. }
  462. function changeAvatar(element, src, animation = false) {
  463. if (element.ruaLoading) {
  464. return
  465. }
  466. if (!element.dataset.orgSrc) {
  467. const orgSrc = element.dataset.src || element.src
  468. element.dataset.orgSrc = orgSrc
  469. }
  470. element.ruaLoading = true
  471. const imgOnloadHandler = () => {
  472. element.ruaLoading = false
  473. removeClass(element, "rua_fadeout")
  474. removeEventListener(element, "load", imgOnloadHandler)
  475. removeEventListener(element, "error", imgOnloadHandler)
  476. }
  477. addEventListener(element, "load", imgOnloadHandler)
  478. addEventListener(element, "error", imgOnloadHandler)
  479. const width = element.clientWidth
  480. const height = element.clientHeight
  481. if (width > 1) {
  482. element.style.width = width + "px"
  483. }
  484. if (height > 1) {
  485. element.style.height = height + "px"
  486. }
  487. if (animation) {
  488. addClass(element, "rua_fadeout")
  489. } else {
  490. element.src =
  491. "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"
  492. }
  493. setTimeout(() => {
  494. element.src = src
  495. })
  496. if (element.dataset.src) {
  497. element.dataset.src = src
  498. }
  499. }
  500. function scanAvatars() {
  501. const avatars = $$('.avatar,a[href*="/member/"] img')
  502. for (const avatar of avatars) {
  503. let userName = avatar.dataset.ruaUserName
  504. if (!userName) {
  505. userName = getUserName(avatar)
  506. if (!userName) {
  507. console.error("Can't get username", avatar, userName)
  508. continue
  509. }
  510. avatar.dataset.ruaUserName = userName
  511. }
  512. const newAvatarSrc = getChangedAavatar(userName)
  513. if (newAvatarSrc && avatar.src !== newAvatarSrc) {
  514. changeAvatar(avatar, newAvatarSrc)
  515. }
  516. }
  517. }
  518. async function main() {
  519. if ($("#rua_tyle")) {
  520. return
  521. }
  522. runWhenHeadExists(() => {
  523. addElement2("style", {
  524. textContent: content_default,
  525. id: "rua_tyle",
  526. })
  527. })
  528. addEventListener(doc, "mouseover", (event) => {
  529. const target = event.target
  530. if (!isAvatar(target)) {
  531. return
  532. }
  533. addChangeButton(target)
  534. })
  535. await initStorage({
  536. avatarValueChangeListener() {
  537. scanAvatars()
  538. },
  539. })
  540. scanAvatars()
  541. const observer = new MutationObserver(
  542. throttle(async () => {
  543. scanAvatars()
  544. }, 500)
  545. )
  546. observer.observe(doc, {
  547. childList: true,
  548. subtree: true,
  549. })
  550. }
  551. main()
  552. })()