// ==UserScript==
// @name NGA 图片浏览器
// @namespace https://greasyfork.org/zh-CN/users/164691-shy07
// @version 1.81
// @description 收集指定楼层的图片,改善图片浏览体验,并支持批量下载
// @author Shy07
// @match *://nga.178.com/*
// @match *://bbs.ngacn.cc/*
// @match *://bbs.nga.cn/*
// @match *://ngabbs.com/*
// @grant GM.xmlHttpRequest
// @grant GM_xmlHttpRequest
// jshint esversion:6
// ==/UserScript==
((ui, self) => {
'use strict'
if (ui === undefined) return
let imageSources = []
let currentImage = 0
let napTimer = null
let zoomRate = 1
let rotateDeg = 0
const callerId = '_shy07_gallery_caller'
const containerClass = '_shy07_gallery_container'
const imgClass = '_shy07_gallery_img'
const progressID = '_shy07_progress_id'
const topLeftMenuID = '_shy07_top_left_menu'
const leftArrowID = '_shy07_left_arrow'
const rightArrowID = '_shy07_right_arrow'
const closeBtnID = '_shy07_close_btn'
const galleryContainerStyle = `
display: flex;
justify-content: center;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
position: fixed;
background: rgba(0, 0, 0, 0.9);
`
const galleryImgStyle = `
top: 0;
left: 0;
width: auto;
height: 100vh;
transition: all .4s linear;
`
const arrowStyle = `
display: block;
position: fixed;
top: 0;
line-height: 100vh;
color: rgba(255, 255, 255, 0.6);
font-size: 5rem;
text-decoration-line: none;
`
const leftArrowStyle = `
left: 0;
padding-left: 1rem;
`
const rightArrowStyle = `
right: 0;
padding-right: 1rem;
`
const closeBtnStyle = `
display: text-block;
position: fixed;
top: 0;
right: 0;
padding: .5rem 1rem;
color: #fff;
text-decoration-line: none;
`
const topLeftMenuStyle = `
display: flex;
justify-content: center;
position: fixed;
top: 0;
left: 0;
width: 100vw;
padding: .5rem 1rem;
background: rgba(0, 0, 0, 0.3);
transition: all .4s linear;
`
const topLeftMenuItemStyle = `
display: text-block;
margin-right: 1rem;
color: #fff;
text-decoration-line: none;
`
const showCollapseContent = container => {
const elements = container.querySelectorAll('.collapse_btn')
if (elements && elements.length > 0) {
elements.forEach(ele => {
const btn = ele.querySelector('button')
btn && btn.click()
})
}
}
const setImageSrc = (index, ele = null) => {
const progress = document.querySelector('#' + progressID)
if (progress) progress.innerHTML = `${index + 1}/${imageSources.length}`
const src = imageSources[index]
const img = ele || document.querySelector('.' + imgClass)
if (img) {
img.src = src
img.style.transform = 'scale(1)'
}
wakeAll()
if (napTimer) clearTimeout(napTimer)
napTimer = setTimeout(napAll, 1000 * 5)
}
const prevImage = ev => {
currentImage = currentImage === 0 ? imageSources.length - 1 : currentImage - 1
setImageSrc(currentImage)
ev.stopPropagation()
}
const nextImage = ev => {
currentImage = currentImage === imageSources.length - 1 ? 0 : currentImage + 1
setImageSrc(currentImage)
ev.stopPropagation()
}
const handleKeydown = ev => {
const code = ev.keyCode
if (code === 27) {
hideGallery()
ev.preventDefault()
} else if (code === 37) {
prevImage()
ev.preventDefault()
} else if (code === 39) {
nextImage()
ev.preventDefault()
}
}
function throttle (final, delay, time) {
let timeout = null
let startTime = new Date()
let skip = false
return e => {
e.preventDefault()
e.stopPropagation()
let curTime = new Date()
clearTimeout(timeout)
if (curTime - startTime >= time) {
if (!skip) {
final(e)
setTimeout(() => (skip = false), delay)
}
skip = true
startTime = curTime
} else {
!skip && (timeout = setTimeout(() => final(e), time))
}
}
}
const rotateImage = ev => {
ev.stopPropagation()
const img = document.querySelector('.' + imgClass)
if (img) {
rotateDeg += 90
img.style.transform = `scale(${zoomRate}) rotate(${rotateDeg}deg)`
}
}
const zoomIn = () => {
const img = document.querySelector('.' + imgClass)
if (img) {
zoomRate = zoomRate === 5 ? zoomRate : zoomRate + .25
img.style.transform = `scale(${zoomRate}) rotate(${rotateDeg}deg)`
}
}
const zoomOut = () => {
const img = document.querySelector('.' + imgClass)
if (img) {
zoomRate = zoomRate === .25 ? zoomRate : zoomRate - .25
img.style.transform = `scale(${zoomRate}) rotate(${rotateDeg}deg)`
}
}
const handleMousewheel = throttle(e => {
if (e.wheelDelta) { // IE/Opera/Chrome
if (e.wheelDelta > 0) {
zoomIn()
}
if (e.wheelDelta < 0) {
zoomOut()
}
} else if (e.detail) { // Firefox
if (e.detail > 0) {
zoomIn()
}
if (e.detail < 0) {
zoomOut()
}
}
}, 400, 400)
const ajaxDownload = (url, callback, args, tryTimes = 0) => {
const GM_download = GM.xmlHttpRequest || GM_xmlHttpRequest
const clearUrl = url.replace(/[&\?]?download_timestamp=\d+/, '')
const retryUrl = clearUrl + (clearUrl.indexOf('?') === -1 ? '?' : '&') + 'download_timestamp=' + new Date().getTime()
GM_download({
method: 'GET',
responseType: 'blob',
url: url,
onreadystatechange: (responseDetails) => {
if (responseDetails.readyState === 4) {
if (responseDetails.status === 200 || responseDetails.status === 304 || responseDetails.status === 0) {
const blob = responseDetails.response
const size = blob && blob.size
if (size && (size / 1024 >= 5)) {
callback(blob, args)
} else if (tryTimes++ === 3) {
callback(blob, args)
} else {
ajaxDownload(retryUrl, callback, args, tryTimes)
}
} else {
if (tryTimes++ === 3) {
callback(null, args)
} else {
ajaxDownload(retryUrl, callback, args, tryTimes)
}
}
}
},
onerror: (responseDetails) => {
if (tryTimes++ === 3) {
callback(null, args)
} else {
ajaxDownload(retryUrl, callback, args, tryTimes)
}
console.log(responseDetails.status)
}
})
}
const downloadBlobFile = (content, fileName) => {
if ('msSaveOrOpenBlob' in navigator) {
navigator.msSaveOrOpenBlob(content, fileName)
} else {
const aLink = document.createElement('a')
aLink.download = fileName
aLink.style = 'display:none;'
const blob = new Blob([content])
aLink.href = window.URL.createObjectURL(blob)
document.body.appendChild(aLink)
if (document.all) {
aLink.click() //IE
} else {
const evt = document.createEvent('MouseEvents')
evt.initEvent('click', true, true)
aLink.dispatchEvent(evt) // 其它浏览器
}
window.URL.revokeObjectURL(aLink.href)
document.body.removeChild(aLink)
}
}
const downloadImage = ev => {
const url = imageSources[currentImage]
const filename = url.split('/').pop()
ajaxDownload(url, downloadBlobFile, filename)
ev.stopPropagation()
}
const downloadAllImage = (blob = null, { list, filename } = {}) => {
if (blob && filename) downloadBlobFile(blob, filename)
const [first, ...newList] = list || imageSources
if (!first) return
const f = first.split('/').pop()
ajaxDownload(first, downloadAllImage, { list: newList, filename: f })
}
const handleDownloadAll = ev => {
downloadAllImage()
ev.stopPropagation()
}
const createGalleryImage = () => {
currentImage = 0
const img = document.createElement('img')
img.className = imgClass
img.style = galleryImgStyle
img.addEventListener('wheel', handleMousewheel)
setImageSrc(0, img)
return img
}
const createLeftArrow = () => {
const ele = document.createElement('a')
ele.id = leftArrowID
ele.style = arrowStyle + leftArrowStyle
ele.innerHTML = '<'
ele.href = 'javascript:;'
ele.addEventListener('click', prevImage)
ele.addEventListener('mouseover', wakeAll)
ele.addEventListener('mouseleave', napAll)
return ele
}
const createRightArrow = () => {
const ele = document.createElement('a')
ele.id = rightArrowID
ele.style = arrowStyle + rightArrowStyle
ele.innerHTML = '>'
ele.href = 'javascript:;'
ele.addEventListener('click', nextImage)
ele.addEventListener('mouseover', wakeAll)
ele.addEventListener('mouseleave', napAll)
return ele
}
const createCloseBtn = () => {
const ele = document.createElement('a')
ele.id = closeBtnID
ele.style = closeBtnStyle
ele.innerHTML = '关闭'
ele.href = 'javascript:;'
ele.addEventListener('click', hideGallery)
ele.addEventListener('mouseover', wakeAll)
ele.addEventListener('mouseleave', napAll)
return ele
}
const createTopLeftMenu = () => {
const ele = document.createElement('div')
ele.id = topLeftMenuID
ele.style = topLeftMenuStyle
const progress = document.createElement('span')
progress.id = progressID
progress.style= topLeftMenuItemStyle
progress.innerHTML = `${currentImage + 1}/${imageSources.length}`
const downloadBtn = document.createElement('a')
downloadBtn.style= topLeftMenuItemStyle
downloadBtn.innerHTML = '下载'
downloadBtn.href = 'javascript:;'
downloadBtn.addEventListener('click', downloadImage)
const downloadAllBtn = document.createElement('a')
downloadAllBtn.style= topLeftMenuItemStyle
downloadAllBtn.innerHTML = '全部下载'
downloadAllBtn.href = 'javascript:;'
downloadAllBtn.addEventListener('click', handleDownloadAll)
const clockwiseBtn = document.createElement('a')
clockwiseBtn.style= topLeftMenuItemStyle
clockwiseBtn.innerHTML = '旋转'
clockwiseBtn.href = 'javascript:;'
clockwiseBtn.addEventListener('click', rotateImage)
ele.appendChild(progress)
ele.appendChild(downloadBtn)
ele.appendChild(downloadAllBtn)
ele.appendChild(clockwiseBtn)
ele.addEventListener('mouseover', wakeAll)
ele.addEventListener('mouseleave', napAll)
return ele
}
const napElement = id => () => {
const ele = document.querySelector('#' + id)
if (ele) ele.style.opacity = 0
}
const wakeElement = id => () => {
const ele = document.querySelector('#' + id)
if (ele) ele.style.opacity = 1
}
const napAll = () => {
napElement(topLeftMenuID)()
napElement(leftArrowID)()
napElement(rightArrowID)()
napElement(closeBtnID)()
}
const wakeAll = () => {
wakeElement(topLeftMenuID)()
wakeElement(leftArrowID)()
wakeElement(rightArrowID)()
wakeElement(closeBtnID)()
}
const showGallery = () => {
if (!imageSources.length) {
window.alert('这层楼没有图片  ̄□ ̄||')
return
}
document.addEventListener('keydown', handleKeydown)
const galleryMask = document.querySelector('.' + containerClass)
if (galleryMask) {
currentImage = 0
setImageSrc(currentImage)
galleryMask.style = galleryContainerStyle
} else {
const ele = document.createElement('div')
ele.className = containerClass
ele.style = galleryContainerStyle
ele.addEventListener('click', hideGallery)
const img = createGalleryImage()
const leftArrow = createLeftArrow()
const rightArrow = createRightArrow()
const closeBtn = createCloseBtn()
const topLeftMenu = createTopLeftMenu()
ele.appendChild(img)
ele.appendChild(leftArrow)
ele.appendChild(rightArrow)
ele.appendChild(topLeftMenu)
ele.appendChild(closeBtn)
document.body.appendChild(ele)
}
}
const hideGallery = () => {
document.removeEventListener('keydown', handleKeydown)
const galleryMask = document.querySelector('.' + containerClass)
if (galleryMask) {
galleryMask.style.display = 'none'
}
}
const getExtname = url => {
const filename = url.split('/').pop()
const extname = filename.split('.').pop()
return extname
}
const getOriginFile = srcUrl => {
const fileExtname = getExtname(srcUrl)
const url = srcUrl
.replace(`.medium.${fileExtname}`, '')
.replace(`.thumb.${fileExtname}`, '')
.replace(`.thumb_s.${fileExtname}`, '')
.replace(`.thumb_ss.${fileExtname}`, '')
return url
}
const collectImages = container => {
showCollapseContent(container)
imageSources = []
const imgs = container.querySelectorAll('.postcontent img')
const temp = []
imgs.forEach(img => {
const src = img.src
const lazySrc = img.dataset ? img.dataset.srclazy : ''
if (lazySrc) {
const url = getOriginFile(lazySrc)
if (!imageSources.includes(url)) imageSources.push(url)
return
}
if (src.includes('/attachments/')) {
const arr = img.src.replace(/https:/g, 'http:').split('http:')
const tmp = arr.filter(s => !!s)[0]
const url = getOriginFile(tmp)
if (!imageSources.includes(url)) imageSources.push(url)
}
})
}
const callerButton = container => {
const a = document.createElement('a')
const handleClick = () => {
collectImages(container.parentElement)
showGallery()
}
a.addEventListener('click', handleClick)
a.id = callerId + container.id
a.className = 'small_colored_text_btn block_txt_c0 stxt'
a.href = 'javascript:;'
a.title = '图片浏览'
a.innerHTML = `
<span>
<span style="font-family: comm_glyphs; -webkit-font-smoothing: antialiased; line-height: 1em;">
图集
</span>
</span>
`
return a
}
const createCallerButton = postData => {
const checkExist = document.querySelector('#' + callerId + postData.i)
if (!checkExist) {
const container = postData.pInfoC
container.appendChild(callerButton(container))
}
}
if (ui.postArg) {
Object.keys(ui.postArg.data).forEach(key => {
createCallerButton(ui.postArg.data[key])
})
}
// 钩子
const hookFunction = (object, functionName, callback) => {
((originalFunction) => {
object[functionName] = function () {
const returnValue = originalFunction.apply(this, arguments)
callback.apply(this, [returnValue, originalFunction, arguments])
return returnValue
}
})(object[functionName])
}
let initialized = false
hookFunction(ui, 'eval', () => {
if (initialized) return
if (ui.postDisp) {
hookFunction(
ui,
'postDisp',
(returnValue, originalFunction, args) => {
createCallerButton(ui.postArg.data[args[0]])
}
)
initialized = true
}
})
})(commonui, __CURRENT_UID)