Greasy Fork is available in English.

X Long Image Enlarge Tool

Add a button on the left top conner of a image,for convenience of displaying long images on X

  1. // ==UserScript==
  2. // @name X Long Image Enlarge Tool
  3. // @name:zh-CN 推特长图放大
  4. // @namespace https://github.com/yanzhili/xImageEnlarge
  5. // @version 2024-07-19
  6. // @description Add a button on the left top conner of a image,for convenience of displaying long images on X
  7. // @description:zh-CN 在图片右上角显示一个放大按钮,方便显示推特中的长图
  8. // @author James.Yan
  9. // @match https://x.com/*
  10. // @match https://twitter.com/*
  11. // @icon https://www.google.com/s2/favicons?sz=64&domain=x.com
  12. // @grant none
  13. // @license MIT
  14. // ==/UserScript==
  15.  
  16. ;(function () {
  17. 'use strict'
  18.  
  19. window.dev = false
  20. const MatchPic = 'pbs.twimg.com/media'
  21. const MarkTag = 'zx-btn-added'
  22. const ImgClass = 'zx-img-class'
  23. const ImgBtnClass = 'zx-btn-class'
  24. const aHrefMap = new Map()
  25. const imgMap = new Map()
  26.  
  27. function main() {
  28. initCss()
  29. initFullscreenDiv()
  30. observeHtml((imgElement) => {
  31. imgElement.classList.add(ImgClass)
  32. const largeImg = imgToLarge(imgElement.src)
  33. distribute(imgElement, largeImg)
  34. addButton(imgElement.parentElement, largeImg)
  35. })
  36. }
  37.  
  38. function distribute(element, src) {
  39. if (!element) return
  40. if (element.parentElement && element.parentElement.tagName == 'A') {
  41. const aHref = element.parentElement.getAttribute('href')
  42. if (aHref.indexOf('/photo/') > -1) {
  43. const aHrefArr = aHref.split('/photo/')
  44. if (aHrefArr.length < 1) return
  45. log(aHrefArr[0], 'keyHref')
  46. const keyHref = aHrefArr[0]
  47. const index = Number(aHrefArr[1])
  48. if (index <= 0) return
  49. if (!imgMap.has(src)) {
  50. imgMap.set(src, keyHref)
  51. }
  52. if (!aHrefMap.has(keyHref)) {
  53. let imgArr = []
  54. imgArr[index - 1] = src
  55. aHrefMap.set(keyHref, imgArr)
  56. } else {
  57. const imgArr = aHrefMap.get(keyHref)
  58. if (imgArr.indexOf(src) < 0) {
  59. imgArr[index - 1] = src
  60. aHrefMap.set(keyHref, imgArr)
  61. }
  62. }
  63. }
  64. } else {
  65. distribute(element.parentElement, src)
  66. }
  67. }
  68.  
  69. function isLongImage(imgSrc) {
  70. if (
  71. imgSrc.indexOf('name=4096x4096') > -1 ||
  72. imgSrc.indexOf('name=large') > -1
  73. ) {
  74. return true
  75. }
  76. return false
  77. }
  78.  
  79. function addButton(parentElement, imgSrc) {
  80. if (parentElement.id.indexOf('zx-') > -1) {
  81. return
  82. }
  83.  
  84. if (parentElement.getAttribute(MarkTag)) {
  85. log(imgSrc, 'Btn-Added')
  86. return
  87. }
  88.  
  89. let button = document.createElement('div')
  90. button.id = genRandomID('btn')
  91. button.className = ImgBtnClass
  92. button.style =
  93. 'width:50px;height:22px;line-height:22px;text-align: center;font-size:12px;font-famliy:ui-monospace;cursor: pointer;color: white;'
  94. button.style.backgroundColor = '#000'
  95. button.style.opacity = '0.7'
  96. button.innerText = 'ZOOM'
  97. button.onclick = (e) => {
  98. e.preventDefault()
  99. e.stopPropagation()
  100. displayFullScreenImg(imgSrc)
  101. }
  102. parentElement.setAttribute(MarkTag, 'added')
  103. parentElement.appendChild(button)
  104. }
  105.  
  106. function isMediaImg(img) {
  107. return img.indexOf(MatchPic) > -1
  108. }
  109.  
  110. function observeHtml(imgAddedCallback) {
  111. const targetNode = document.body
  112. const config = {
  113. attributes: true,
  114. childList: true,
  115. subtree: true,
  116. attributeFilter: ['src'],
  117. }
  118. const callback = function (mutationsList, observer) {
  119. for (let mutation of mutationsList) {
  120. if (
  121. mutation.attributeName == 'src' &&
  122. isMediaImg(mutation.target.src)
  123. ) {
  124. imgAddedCallback(mutation.target)
  125. }
  126. }
  127. }
  128. const observer = new MutationObserver(callback)
  129. observer.observe(targetNode, config)
  130. // observer.disconnect()
  131. }
  132.  
  133. function imgToLarge(imgSrc) {
  134. if (!imgSrc) return
  135. let nameValue = ''
  136. if (imgSrc.indexOf('name=') > -1) {
  137. let arr1 = imgSrc.split('?')
  138. if ((arr1.length = 2)) {
  139. let str1 = arr1[1]
  140. let arr2 = str1.split('&')
  141. if (arr2) {
  142. arr2.map((i) => {
  143. let arr3 = i.split('=')
  144. if (arr3.length == 2 && arr3[0] == 'name' && arr3[1] != 'large') {
  145. let tempNameValue = arr3[1]
  146. if (tempNameValue.indexOf('x') > -1) {
  147. let arr4 = tempNameValue.split('x')
  148. //大图不替换
  149. if (Number(arr4[0]) < 1024) {
  150. nameValue = arr3[1]
  151. }
  152. } else {
  153. nameValue = arr3[1]
  154. }
  155. }
  156. })
  157. }
  158. }
  159. }
  160. if (nameValue) {
  161. imgSrc = imgSrc.replace(`name=${nameValue}`, 'name=large')
  162. }
  163. log(imgSrc, 'Large-Imgage-Src')
  164. return imgSrc
  165. }
  166.  
  167. window.onload = function () {
  168. main()
  169. }
  170.  
  171. const ImgFullWidthCss = 'width:100%;height:auto;margin:auto;cursor:zoom-in'
  172. const ImgFullHeightCss = 'width:auto;height:100%;margin:auto;cursor:zoom-in'
  173. const ImgAutoCss = 'width:auto;height:auto;margin:auto;cursor:zoom-in'
  174. const FSDivId = 'zx-fullsceen-div-id'
  175. const FSImgId = 'zx-fullsceen-img-id'
  176. const ImgDisplayType = 'zx-display-type'
  177. const NextArrowId = 'zx-next-arrow-id'
  178. const PreviousArrowId = 'zx-previous-arrow-id'
  179.  
  180. function initFullscreenDiv() {
  181. let fsDiv = document.createElement('div')
  182. fsDiv.id = FSDivId
  183. fsDiv.style =
  184. 'text-align: center;width:100%;height:100%; position: fixed;top: 0px;bottom: 0px;overflow-y:auto;display:none'
  185. fsDiv.style.backgroundColor = 'black'
  186. fsDiv.onclick = () => {
  187. dismissImg()
  188. }
  189. let imgElmt = document.createElement('img')
  190. imgElmt.id = FSImgId
  191. imgElmt.onclick = (e) => {
  192. e.preventDefault()
  193. e.stopPropagation()
  194. if (imgElmt.getAttribute(ImgDisplayType) == 'image-auto') {
  195. imgElmt.setAttribute(ImgDisplayType, 'image-fw')
  196. imgElmt.style = ImgFullWidthCss
  197. } else if (imgElmt.getAttribute(ImgDisplayType) == 'image-fw') {
  198. imgElmt.setAttribute(ImgDisplayType, 'image-fh')
  199. imgElmt.style = ImgFullHeightCss
  200. } else if (imgElmt.getAttribute(ImgDisplayType) == 'image-fh') {
  201. imgElmt.setAttribute(ImgDisplayType, 'image-auto')
  202. imgElmt.style = ImgAutoCss
  203. }
  204. }
  205.  
  206. fsDiv.appendChild(imgElmt)
  207.  
  208. fsDiv.appendChild(genCloseButton())
  209. fsDiv.appendChild(genArrowButton(true))
  210. fsDiv.appendChild(genArrowButton(false))
  211. document.body.appendChild(fsDiv)
  212. }
  213.  
  214. function genCloseButton() {
  215. let closeImgDiv = document.createElement('div')
  216. closeImgDiv.style =
  217. 'top: 0;right: 0px;margin-top: 10px;margin-right: 10px;width: 40px;height: 40px;position: fixed;'
  218.  
  219. closeImgDiv.style.backgroundColor = '#000'
  220. closeImgDiv.style.opacity = '0.8'
  221. closeImgDiv.style.backgroundImage =
  222. 'url(data:image/svg+xml;base64,PHN2ZyB0PSIxNzE5NTU1MTg1MTM4IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjE0MDAzIiB3aWR0aD0iMzIiIGhlaWdodD0iMzIiPjxwYXRoIGQ9Ik01MTAuODA5NiA0MjAuMzAwOGwzMzUuMjk2LTMzNS4yOTYgOTAuNTA4OCA5MC41MDg4LTMzNS4yOTYgMzM1LjI5NiAzMzUuMjk2IDMzNS4yOTYtOTAuNTA4OCA5MC41MDg4LTMzNS4yOTYtMzM1LjI5Ni0zMzUuMjk2IDMzNS4yOTYtOTAuNTA4OC05MC41MDg4IDMzNS4yOTYtMzM1LjI5Ni0zMzUuMjk2LTMzNS4yOTYgOTAuNTA4OC05MC41MDg4eiIgZmlsbD0iI2ZmZmZmZiIgcC1pZD0iMTQwMDQiPjwvcGF0aD48L3N2Zz4=)'
  223. closeImgDiv.style.backgroundRepeat = 'no-repeat'
  224. closeImgDiv.style.backgroundSize = 'cover'
  225. closeImgDiv.style.cursor = 'pointer'
  226. closeImgDiv.onclick = () => {
  227. dismissImg()
  228. }
  229. return closeImgDiv
  230. }
  231.  
  232. function genArrowButton(showNext) {
  233. let arrowDiv = document.createElement('div')
  234. if (showNext) {
  235. arrowDiv.id = NextArrowId
  236. } else {
  237. arrowDiv.id = PreviousArrowId
  238. }
  239. if (showNext) {
  240. arrowDiv.style = 'right: 0px; margin-right: 10px;'
  241. } else {
  242. arrowDiv.style = 'left: 0px; margin-left: 10px;'
  243. }
  244. arrowDiv.style.display = 'none'
  245. arrowDiv.style.top = '50%'
  246. arrowDiv.style.width = '45px'
  247. arrowDiv.style.height = '45px'
  248. arrowDiv.style.position = 'fixed'
  249. arrowDiv.style.borderRadius = '50%'
  250. arrowDiv.style.backgroundColor = '#80808091'
  251. arrowDiv.style.backgroundImage =
  252. 'url(data:image/svg+xml;base64,PHN2ZyB0PSIxNzIwNDMwNjM5Mzg3IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjgxMzgiIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiI+PHBhdGggZD0iTTU0NCA2OTAuNzczMzMzbDExNi4wNTMzMzMtMTE2LjA1MzMzM2EzMiAzMiAwIDEgMSA0NS4yMjY2NjcgNDUuMjI2NjY3bC0xNzAuNjY2NjY3IDE3MC42NjY2NjZhMzIgMzIgMCAwIDEtNDUuMjI2NjY2IDBsLTE3MC42NjY2NjctMTcwLjY2NjY2NmEzMiAzMiAwIDEgMSA0NS4yMjY2NjctNDUuMjI2NjY3bDExNi4wNTMzMzMgMTE2LjA1MzMzM1YyNzcuMzMzMzMzYTMyIDMyIDAgMCAxIDY0IDB2NDEzLjQ0eiIgZmlsbD0iI2ZmZmZmZiIgcC1pZD0iODEzOSI+PC9wYXRoPjwvc3ZnPg==)'
  253. arrowDiv.style.backgroundRepeat = 'no-repeat'
  254. arrowDiv.style.backgroundSize = 'cover'
  255. arrowDiv.style.cursor = 'pointer'
  256. if (showNext) {
  257. arrowDiv.style.transform = 'rotate(-90deg)'
  258. } else {
  259. arrowDiv.style.transform = 'rotate(90deg)'
  260. }
  261. arrowDiv.onclick = (e) => {
  262. e.preventDefault()
  263. e.stopPropagation()
  264. if (showNext) {
  265. showNextImg(true)
  266. } else {
  267. showNextImg(false)
  268. }
  269. }
  270. return arrowDiv
  271. }
  272.  
  273. function showNextImg(showNext) {
  274. let imgElmt = document.getElementById(FSImgId)
  275. const imgSrc = imgElmt.getAttribute('src')
  276. const keyHref = imgMap.get(imgSrc)
  277. log(keyHref, imgSrc)
  278. if (!keyHref) return
  279. const imgArr = aHrefMap.get(keyHref)
  280. log(imgArr, keyHref)
  281. if (!imgArr) return
  282. const imgIndex = imgArr.indexOf(imgSrc)
  283. const len = imgArr.length
  284.  
  285. let fsDiv = document.getElementById(FSDivId)
  286. const windowRatio = fsDiv.clientWidth / fsDiv.clientHeight
  287. let imgUrl
  288. if (showNext) {
  289. if (imgIndex + 1 + 1 <= len) {
  290. imgUrl = imgArr[imgIndex + 1]
  291. }
  292. } else {
  293. if (imgIndex > 0) {
  294. imgUrl = imgArr[imgIndex - 1]
  295. }
  296. }
  297. if (!imgUrl) return
  298. checkHasNextAndHasPrevious(imgUrl)
  299. displayImg(imgUrl, windowRatio)
  300. }
  301.  
  302. function initCss() {
  303. var css = `
  304. .${ImgBtnClass}
  305. {display:none}
  306. .${ImgClass}:hover + .${ImgBtnClass}, .${ImgBtnClass}:hover
  307. { display: inline-block }
  308. `
  309. var style = document.createElement('style')
  310.  
  311. if (style.styleSheet) {
  312. style.styleSheet.cssText = css
  313. } else {
  314. style.appendChild(document.createTextNode(css))
  315. }
  316.  
  317. document.getElementsByTagName('head')[0].appendChild(style)
  318. }
  319.  
  320. function displayFullScreenImg(imgSrc) {
  321. let fsDiv = document.getElementById(FSDivId)
  322. fsDiv.style.overflowY = 'overflow-y:auto'
  323. fsDiv.style.display = 'flex'
  324. fsDiv.style.justifyContent = 'center'
  325. fsDiv.style.alignItems = 'center'
  326. const windowRatio = fsDiv.clientWidth / fsDiv.clientHeight
  327. displayImg(imgSrc, windowRatio)
  328. checkHasNextAndHasPrevious(imgSrc)
  329. }
  330.  
  331. function checkHasNextAndHasPrevious(imgSrc) {
  332. clearEmptyImg(imgSrc)
  333. const nextBtn = document.getElementById(NextArrowId)
  334. const previousBtn = document.getElementById(PreviousArrowId)
  335. nextBtn.style.display = 'none'
  336. previousBtn.style.display = 'none'
  337. if (hasNext(imgSrc)) {
  338. nextBtn.style.display = 'block'
  339. }
  340. if (hasPrevious(imgSrc)) {
  341. if (previousBtn) {
  342. previousBtn.style.display = 'block'
  343. }
  344. }
  345. }
  346.  
  347. function clearEmptyImg(imgSrc) {
  348. const keyHref = imgMap.get(imgSrc)
  349. if (!keyHref) return
  350. const imgArr = aHrefMap.get(keyHref)
  351. if (imgArr) {
  352. const newImgArr = imgArr.filter((i) => {
  353. return !!i
  354. })
  355. aHrefMap.set(keyHref, newImgArr)
  356. }
  357. }
  358.  
  359. function hasNext(imgSrc) {
  360. const keyHref = imgMap.get(imgSrc)
  361. if (!keyHref) return false
  362. const imgArr = aHrefMap.get(keyHref)
  363. if (!imgArr) return false
  364. const imgIndex = imgArr.indexOf(imgSrc)
  365. const len = imgArr.length
  366. if (len == 1) {
  367. return false
  368. }
  369. return imgIndex + 1 < len
  370. }
  371.  
  372. function hasPrevious(imgSrc) {
  373. const keyHref = imgMap.get(imgSrc)
  374. if (!keyHref) return false
  375. const imgArr = aHrefMap.get(keyHref)
  376. if (!imgArr) return false
  377. const imgIndex = imgArr.indexOf(imgSrc)
  378. const len = imgArr.length
  379. if (len == 1) {
  380. return false
  381. }
  382.  
  383. return imgIndex + 1 > 1 && imgIndex + 1 <= len
  384. }
  385.  
  386. function displayImg(imgSrc, windowRatio) {
  387. let imgElmt = document.getElementById(FSImgId)
  388. if (!imgElmt) return
  389. imgElmt.setAttribute('src', '')
  390. const img = new Image()
  391. img.onload = function () {
  392. const imgRatio = this.width / this.height
  393. if (imgRatio > windowRatio) {
  394. imgElmt.style = ImgFullWidthCss
  395. imgElmt.setAttribute(ImgDisplayType, 'image-fw')
  396. } else {
  397. imgElmt.style = ImgFullHeightCss
  398. imgElmt.setAttribute(ImgDisplayType, 'image-fh')
  399. }
  400. imgElmt.setAttribute('src', imgSrc)
  401. }
  402. img.src = imgSrc
  403. }
  404.  
  405. function dismissImg() {
  406. let fsDiv = document.getElementById(FSDivId)
  407. fsDiv.style.display = 'none'
  408. let imgElmt = document.getElementById(FSImgId)
  409. if (!imgElmt) return
  410. imgElmt.removeAttribute('src')
  411. }
  412.  
  413. function genRandomID(tag) {
  414. return `zx-${tag}-${Math.random().toString(36).slice(-8)}`
  415. }
  416.  
  417. /**
  418. * 将标准时间格式化
  419. * @param {Date} time 标准时间
  420. * @param {String} format 格式
  421. * @return {String}
  422. */
  423. function moment(time) {
  424. // 获取年⽉⽇时分秒
  425. let y = time.getFullYear()
  426. let m = (time.getMonth() + 1).toString().padStart(2, `0`)
  427. let d = time.getDate().toString().padStart(2, `0`)
  428. let h = time.getHours().toString().padStart(2, `0`)
  429. let min = time.getMinutes().toString().padStart(2, `0`)
  430. let s = time.getSeconds().toString().padStart(2, `0`)
  431. return `${y}-${m}-${d} ${h}:${min}:${s}`
  432. }
  433.  
  434. function log(msg, tag) {
  435. if (!window.dev) {
  436. return false
  437. }
  438. if (tag) {
  439. console.log(`${moment(new Date())} ${tag}`, msg)
  440. } else {
  441. console.log(`${moment(new Date())}`, msg)
  442. }
  443. }
  444. })()