您需要先安装一款用户样式管理器扩展(如 Stylus)后才能安装此样式。
您需要先安装一款用户样式管理器扩展(如 Stylus)后才能安装此样式。
您需要先安装一款用户样式管理器扩展(如 Stylus)后才能安装此样式。
您需要先安装一款用户样式管理器扩展后才能安装此样式。
您需要先安装一款用户样式管理器扩展后才能安装此样式。
您需要先安装一款用户样式管理器扩展后才能安装此样式。
(我已经安装了用户样式管理器,让我安装!)
// ==UserScript==
// @name X Long Image Enlarge Tool
// @name:zh-CN 推特长图放大
// @namespace https://github.com/yanzhili/xImageEnlarge
// @version 2024-07-18
// @description Add a button on the left top conner of a image,for convenience of displaying long images on X
// @description:zh-CN 在图片右上角显示一个放大按钮,方便显示推特中的长图
// @author James.Yan
// @match https://x.com/*
// @match https://twitter.com/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=x.com
// @grant none
// @license MIT
// ==/UserScript==
;(function () {
'use strict'
window.dev = false
const MatchPic = 'pbs.twimg.com/media'
const MarkTag = 'zx-btn-added'
const ImgClass = 'zx-img-class'
const ImgBtnClass = 'zx-btn-class'
const aHrefMap = new Map()
const imgMap = new Map()
function main() {
initCss()
initFullscreenDiv()
observeHtml((imgElement) => {
imgElement.classList.add(ImgClass)
const largeImg = imgToLarge(imgElement.src)
distribute(imgElement, largeImg)
addButton(imgElement.parentElement, largeImg)
})
}
function distribute(element, src) {
if (!element) return
if (element.parentElement && element.parentElement.tagName == 'A') {
const aHref = element.parentElement.getAttribute('href')
if (aHref.indexOf('/photo/') > -1) {
const aHrefArr = aHref.split('/photo/')
if (aHrefArr.length < 1) return
log(aHrefArr[0], 'keyHref')
const keyHref = aHrefArr[0]
const index = Number(aHrefArr[1])
if (index <= 0) return
if (!imgMap.has(src)) {
imgMap.set(src, keyHref)
}
if (!aHrefMap.has(keyHref)) {
let imgArr = []
imgArr[index - 1] = src
aHrefMap.set(keyHref, imgArr)
} else {
const imgArr = aHrefMap.get(keyHref)
if (imgArr.indexOf(src) < 0) {
imgArr[index - 1] = src
aHrefMap.set(keyHref, imgArr)
}
}
}
} else {
distribute(element.parentElement, src)
}
}
function isLongImage(imgSrc) {
if (
imgSrc.indexOf('name=4096x4096') > -1 ||
imgSrc.indexOf('name=large') > -1
) {
return true
}
return false
}
function addButton(parentElement, imgSrc) {
if (parentElement.id.indexOf('zx-') > -1) {
return
}
if (parentElement.getAttribute(MarkTag)) {
log(imgSrc, 'Btn-Added')
return
}
let button = document.createElement('div')
button.id = genRandomID('btn')
button.className = ImgBtnClass
button.style =
'width:50px;height:22px;line-height:22px;text-align: center;font-size:12px;font-famliy:ui-monospace;cursor: pointer;color: white;'
button.style.backgroundColor = '#000'
button.style.opacity = '0.7'
button.innerText = 'ZOOM'
button.onclick = (e) => {
e.preventDefault()
e.stopPropagation()
displayFullScreenImg(imgSrc)
}
parentElement.setAttribute(MarkTag, 'added')
parentElement.appendChild(button)
}
function isMediaImg(img) {
return img.indexOf(MatchPic) > -1
}
function observeHtml(imgAddedCallback) {
const targetNode = document.body
const config = {
attributes: true,
childList: true,
subtree: true,
attributeFilter: ['src'],
}
const callback = function (mutationsList, observer) {
for (let mutation of mutationsList) {
if (
mutation.attributeName == 'src' &&
isMediaImg(mutation.target.src)
) {
imgAddedCallback(mutation.target)
}
}
}
const observer = new MutationObserver(callback)
observer.observe(targetNode, config)
// observer.disconnect()
}
function imgToLarge(imgSrc) {
if (!imgSrc) return
let nameValue = ''
if (imgSrc.indexOf('name=') > -1) {
let arr1 = imgSrc.split('?')
if ((arr1.length = 2)) {
let str1 = arr1[1]
let arr2 = str1.split('&')
if (arr2) {
arr2.map((i) => {
let arr3 = i.split('=')
if (arr3.length == 2 && arr3[0] == 'name' && arr3[1] != 'large') {
let tempNameValue = arr3[1]
if (tempNameValue.indexOf('x') > -1) {
let arr4 = tempNameValue.split('x')
//大图不替换
if (Number(arr4[0]) < 1024) {
nameValue = arr3[1]
}
} else {
nameValue = arr3[1]
}
}
})
}
}
}
if (nameValue) {
imgSrc = imgSrc.replace(`name=${nameValue}`, 'name=large')
}
log(imgSrc, 'Large-Imgage-Src')
return imgSrc
}
window.onload = function () {
main()
}
const ImgFullWidthCss = 'width:100%;height:auto;margin:auto;cursor:zoom-in'
const ImgFullHeightCss = 'width:auto;height:100%;margin:auto;cursor:zoom-in'
const ImgAutoCss = 'width:auto;height:auto;margin:auto;cursor:zoom-in'
const FSDivId = 'zx-fullsceen-div-id'
const FSImgId = 'zx-fullsceen-img-id'
const ImgDisplayType = 'zx-display-type'
const NextArrowId = 'zx-next-arrow-id'
const PreviousArrowId = 'zx-previous-arrow-id'
function initFullscreenDiv() {
let fsDiv = document.createElement('div')
fsDiv.id = FSDivId
fsDiv.style =
'text-align: center;width:100%;height:100%; position: fixed;top: 0px;bottom: 0px;overflow-y:auto;display:none'
fsDiv.style.backgroundColor = 'black'
fsDiv.onclick = () => {
dismissImg()
}
let imgElmt = document.createElement('img')
imgElmt.id = FSImgId
imgElmt.onclick = (e) => {
e.preventDefault()
e.stopPropagation()
if (imgElmt.getAttribute(ImgDisplayType) == 'image-auto') {
imgElmt.setAttribute(ImgDisplayType, 'image-fw')
imgElmt.style = ImgFullWidthCss
} else if (imgElmt.getAttribute(ImgDisplayType) == 'image-fw') {
imgElmt.setAttribute(ImgDisplayType, 'image-fh')
imgElmt.style = ImgFullHeightCss
} else if (imgElmt.getAttribute(ImgDisplayType) == 'image-fh') {
imgElmt.setAttribute(ImgDisplayType, 'image-auto')
imgElmt.style = ImgAutoCss
}
}
fsDiv.appendChild(imgElmt)
fsDiv.appendChild(genCloseButton())
fsDiv.appendChild(genArrowButton(true))
fsDiv.appendChild(genArrowButton(false))
document.body.appendChild(fsDiv)
}
function genCloseButton() {
let closeImgDiv = document.createElement('div')
closeImgDiv.style =
'top: 0;right: 0px;margin-top: 10px;margin-right: 10px;width: 40px;height: 40px;position: fixed;'
closeImgDiv.style.backgroundColor = '#000'
closeImgDiv.style.opacity = '0.8'
closeImgDiv.style.backgroundImage =
'url(data:image/svg+xml;base64,PHN2ZyB0PSIxNzE5NTU1MTg1MTM4IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjE0MDAzIiB3aWR0aD0iMzIiIGhlaWdodD0iMzIiPjxwYXRoIGQ9Ik01MTAuODA5NiA0MjAuMzAwOGwzMzUuMjk2LTMzNS4yOTYgOTAuNTA4OCA5MC41MDg4LTMzNS4yOTYgMzM1LjI5NiAzMzUuMjk2IDMzNS4yOTYtOTAuNTA4OCA5MC41MDg4LTMzNS4yOTYtMzM1LjI5Ni0zMzUuMjk2IDMzNS4yOTYtOTAuNTA4OC05MC41MDg4IDMzNS4yOTYtMzM1LjI5Ni0zMzUuMjk2LTMzNS4yOTYgOTAuNTA4OC05MC41MDg4eiIgZmlsbD0iI2ZmZmZmZiIgcC1pZD0iMTQwMDQiPjwvcGF0aD48L3N2Zz4=)'
closeImgDiv.style.backgroundRepeat = 'no-repeat'
closeImgDiv.style.backgroundSize = 'cover'
closeImgDiv.style.cursor = 'pointer'
closeImgDiv.onclick = () => {
dismissImg()
}
return closeImgDiv
}
function genArrowButton(showNext) {
let arrowDiv = document.createElement('div')
if (showNext) {
arrowDiv.id = NextArrowId
} else {
arrowDiv.id = PreviousArrowId
}
if (showNext) {
arrowDiv.style = 'right: 0px; margin-right: 10px;'
} else {
arrowDiv.style = 'left: 0px; margin-left: 10px;'
}
arrowDiv.style.display = 'none'
arrowDiv.style.top = '50%'
arrowDiv.style.width = '45px'
arrowDiv.style.height = '45px'
arrowDiv.style.position = 'fixed'
arrowDiv.style.borderRadius = '50%'
arrowDiv.style.backgroundColor = '#80808091'
arrowDiv.style.backgroundImage =
'url(data:image/svg+xml;base64,PHN2ZyB0PSIxNzIwNDMwNjM5Mzg3IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjgxMzgiIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiI+PHBhdGggZD0iTTU0NCA2OTAuNzczMzMzbDExNi4wNTMzMzMtMTE2LjA1MzMzM2EzMiAzMiAwIDEgMSA0NS4yMjY2NjcgNDUuMjI2NjY3bC0xNzAuNjY2NjY3IDE3MC42NjY2NjZhMzIgMzIgMCAwIDEtNDUuMjI2NjY2IDBsLTE3MC42NjY2NjctMTcwLjY2NjY2NmEzMiAzMiAwIDEgMSA0NS4yMjY2NjctNDUuMjI2NjY3bDExNi4wNTMzMzMgMTE2LjA1MzMzM1YyNzcuMzMzMzMzYTMyIDMyIDAgMCAxIDY0IDB2NDEzLjQ0eiIgZmlsbD0iI2ZmZmZmZiIgcC1pZD0iODEzOSI+PC9wYXRoPjwvc3ZnPg==)'
arrowDiv.style.backgroundRepeat = 'no-repeat'
arrowDiv.style.backgroundSize = 'cover'
arrowDiv.style.cursor = 'pointer'
if (showNext) {
arrowDiv.style.transform = 'rotate(-90deg)'
} else {
arrowDiv.style.transform = 'rotate(90deg)'
}
arrowDiv.onclick = (e) => {
e.preventDefault()
e.stopPropagation()
if (showNext) {
showNextImg(true)
} else {
showNextImg(false)
}
}
return arrowDiv
}
function showNextImg(showNext) {
let imgElmt = document.getElementById(FSImgId)
const imgSrc = imgElmt.getAttribute('src')
const keyHref = imgMap.get(imgSrc)
log(keyHref, imgSrc)
if (!keyHref) return
const imgArr = aHrefMap.get(keyHref)
log(imgArr, keyHref)
if (!imgArr) return
const imgIndex = imgArr.indexOf(imgSrc)
const len = imgArr.length
let fsDiv = document.getElementById(FSDivId)
const windowRatio = fsDiv.clientWidth / fsDiv.clientHeight
let imgUrl
if (showNext) {
if (imgIndex + 1 + 1 <= len) {
imgUrl = imgArr[imgIndex + 1]
}
} else {
if (imgIndex > 0) {
imgUrl = imgArr[imgIndex - 1]
}
}
if (!imgUrl) return
checkHasNextAndHasPrevious(imgUrl)
displayImg(imgUrl, windowRatio)
}
function initCss() {
var css = `
.${ImgBtnClass}
{display:none}
.${ImgClass}:hover + .${ImgBtnClass}, .${ImgBtnClass}:hover
{ display: inline-block }
`
var style = document.createElement('style')
if (style.styleSheet) {
style.styleSheet.cssText = css
} else {
style.appendChild(document.createTextNode(css))
}
document.getElementsByTagName('head')[0].appendChild(style)
}
function displayFullScreenImg(imgSrc) {
let fsDiv = document.getElementById(FSDivId)
fsDiv.style.overflowY = 'overflow-y:auto'
fsDiv.style.display = 'flex'
fsDiv.style.justifyContent = 'center'
fsDiv.style.alignItems = 'center'
const windowRatio = fsDiv.clientWidth / fsDiv.clientHeight
displayImg(imgSrc, windowRatio)
checkHasNextAndHasPrevious(imgSrc)
}
function checkHasNextAndHasPrevious(imgSrc) {
const nextBtn = document.getElementById(NextArrowId)
const previousBtn = document.getElementById(PreviousArrowId)
nextBtn.style.display = 'none'
previousBtn.style.display = 'none'
if (hasNext(imgSrc)) {
nextBtn.style.display = 'block'
}
if (hasPrevious(imgSrc)) {
if (previousBtn) {
previousBtn.style.display = 'block'
}
}
}
function hasNext(imgSrc) {
const keyHref = imgMap.get(imgSrc)
if (!keyHref) return false
const imgArr = aHrefMap.get(keyHref)
if (!imgArr) return false
const imgIndex = imgArr.indexOf(imgSrc)
const len = imgArr.length
if (len == 1) {
return false
}
return imgIndex + 1 < len
}
function hasPrevious(imgSrc) {
const keyHref = imgMap.get(imgSrc)
if (!keyHref) return false
const imgArr = aHrefMap.get(keyHref)
if (!imgArr) return false
const imgIndex = imgArr.indexOf(imgSrc)
const len = imgArr.length
if (len == 1) {
return false
}
return imgIndex + 1 > 1 && imgIndex + 1 <= len
}
function displayImg(imgSrc, windowRatio) {
let imgElmt = document.getElementById(FSImgId)
if (!imgElmt) return
imgElmt.setAttribute('src', '')
const img = new Image()
img.onload = function () {
const imgRatio = this.width / this.height
if (imgRatio > windowRatio) {
imgElmt.style = ImgFullWidthCss
imgElmt.setAttribute(ImgDisplayType, 'image-fw')
} else {
imgElmt.style = ImgFullHeightCss
imgElmt.setAttribute(ImgDisplayType, 'image-fh')
}
imgElmt.setAttribute('src', imgSrc)
}
img.src = imgSrc
}
function dismissImg() {
let fsDiv = document.getElementById(FSDivId)
fsDiv.style.display = 'none'
let imgElmt = document.getElementById(FSImgId)
if (!imgElmt) return
imgElmt.removeAttribute('src')
}
function genRandomID(tag) {
return `zx-${tag}-${Math.random().toString(36).slice(-8)}`
}
/**
* 将标准时间格式化
* @param {Date} time 标准时间
* @param {String} format 格式
* @return {String}
*/
function moment(time) {
// 获取年⽉⽇时分秒
let y = time.getFullYear()
let m = (time.getMonth() + 1).toString().padStart(2, `0`)
let d = time.getDate().toString().padStart(2, `0`)
let h = time.getHours().toString().padStart(2, `0`)
let min = time.getMinutes().toString().padStart(2, `0`)
let s = time.getSeconds().toString().padStart(2, `0`)
return `${y}-${m}-${d} ${h}:${min}:${s}`
}
function log(msg, tag) {
if (!window.dev) {
return false
}
if (tag) {
console.log(`${moment(new Date())} ${tag}`, msg)
} else {
console.log(`${moment(new Date())}`, msg)
}
}
})()