I like to browse one-handed.
Tested on latest Firefox with Greasemonkey.
Index Gallery:
- Next page: [Space] at bottom
- Previous page: [Shift-Space] at top
- Convert to gallery: [Ctrl-Space] or [View all] button
- (Oldest first: [Ctrl-shift-space])
- Next in gallery: [Space]
- Previous in gallery: [Shift-space]
- Play gif or webm: [W]
- Seek: [A]/[D]
- Volume: [Shift-W]/[Shift-S]
Post Tweaks:
- Play webm: [W] (Also focuses image)
- Seek: [A]/[D]
- Volume: [Shift-W]/[Shift-S]
I can make some improvements if there's interest, but I think I saw dedicated extensions for this sort of thing. Figured I'd post what I have anyway.
Index Gallery
Reload gallery page to get out of it, it just replaces the current document
Lazily loads items in sequence, adjust timerInterval if your internet is awesome.
Does not handle flash submissions
Firefox doesn't like pages of videos (Video thumbnails, please)
No blacklist support (I don't know where it is in the document scope)
Rudimentary include/exclude filter commented out/disabled inside of extractItem around line 130
Unviewed/Unloaded shown in tab title
// ==UserScript==
// @name E621_IndexGallery
// @namespace eig
// @include https://e621.net/post/index/*
// @version 1
// @grant none
// ==/UserScript==
let timerInterval = 1000
let baseTitle = document.title
let mainLoop = null
let paused = false
let loading = 0
let onItemLoad = () => {
loading--
}
let onItemError = () => {
loading--
}
let imgOnLoad = function() {
if (this.width + this.height == 0) {
this.onerror()
}
else if (this.complete) {
// console.log('Active:', loading, 'Finished:', this.src)
onItemLoad()
}
}
let imgOnLoadComplete = function() {
if (this.width + this.height == 0) {
this.onerror()
}
else {
// console.log('Active:', loading, 'Finished:', this.src)
onItemLoad()
}
}
let extensions = [
'jpg',
'png',
'jpeg',
'gif'
]
let clickGif = function() {
if (this.hasClassName('gif-loader')) {
this.playing = true
this.src = this.item.base+'.gif'
this.removeClassName('gif-loader')
}
// else {
// this.playing = false
// this.src = this.item.thumb
// this.addClassName('gif-loader')
// }
}
let imgOnError = function() {
this._ext++
let nextExt = extensions[this._ext]
if (nextExt) {
if (nextExt == 'gif') {
this.src = this.item.thumb
this.addClassName('gif-loader')
this.addEventListener('click', clickGif)
// console.log('Loading gif placeholder: '+this.item.base)
}
else {
this.src = this._src+'.'+nextExt
// console.log('Falling back to: '+this.src)
}
}
else {
console.log('Failed to load: '+this.item.page)
onItemError()
}
}
let urls = []
/*
We have to load each video page to discover the url for the video itself, because there's no direct connection to the video
*/
let videoPageLoad = function() {
if (this.status != 200) {
console.log(this.statusText)
// TODO: Error
return
}
else {
this.item.src = this.responseXML.querySelector('#webm-container > source').src
this.item.node.src = this.item.src
}
// console.dir(this.responseXML)
// console.log('Video loaded : '+this.responseURL+', '+this.item.src)
// console.log('videoPageLoad', this)
}
let videoPageError = function() {
// console.log('videoPageError', this)
onItemError()
loading--
}
let videoOnCanPlay = function() {
// console.log('videoOnCanPlay', this)
onItemLoad()
loading--
}
let videoOnError = function() {
// console.log('videoOnError', this)
onItemLoad()
loading--
}
let re_src = /(.+)preview\/(.+)\.jpg$/
let extractItem = e => {
if (e.src.includes('download-preview')) {
return null
}
// if (!(e.alt.includes('female') || e.alt.includes('intersex')) || e.alt.includes('my_little_pony') || e.alt.includes('sonic_(series)')) {
// return null
// }
let item = {
page: e.parentElement.href,
thumb: e.src
}
if (e.src.includes('webm-preview')) {
item.video = true
}
else if (e.src.includes('preview/')) {
let match = re_src.exec(e.src)
if (match) {
item.base = match[1]+match[2]
}
}
return item
}
let extractPreviews = doc => {
return Array.from(doc.querySelectorAll('img.preview'), extractItem)
.filter(x => x)
}
let widgets = {}
let timeoutID
let currentImg
let unviewed = -1
let mainItem = (item) => {
// Wrapper
let div = document.createElement('div')
// Video
if (item.video) {
// console.log('Adding video: '+item.page)
let video = document.createElement('video')
item.node = video
video.loop = true
video.autoplay = false
video.muted = true
video.controls = true
video.preload = 'metadata'
video.type = 'video/webm'
video.addEventListener('canplay', videoOnCanPlay)
video.addEventListener('error', videoOnError)
div.appendChild(video)
// Weight videos heigher
loading++
let xhr = new XMLHttpRequest()
xhr.item = item
xhr.addEventListener('load', videoPageLoad)
xhr.addEventListener('error', videoPageError)
xhr.open('GET', item.page)
xhr.responseType = 'document';
xhr.send()
}
// Image
else {
// console.log('Adding image: '+item.base)
let img = document.createElement('img')
img.onerror = imgOnError
img.onload = imgOnLoad
img.onloadcomplete = imgOnLoadComplete
img.item = item
img.src = item.base+'.jpg'
img._src = item.base
img._ext = 0
div.appendChild(img)
}
// Link
let a = document.createElement('a')
a.href = item.page
a.innerHTML = item.page
div.appendChild(a)
widgets.output.appendChild(div)
loading++
if (!currentImg) {
currentImg = div
currentImg.scrollIntoView({behavior: 'smooth'})
}
else {
div.viewed = false
unviewed++
}
}
let updateUI = () => {
if (urls.length === 0) {
document.title = baseTitle
widgets.status.innerHTML = 'Done'
}
else {
if (unviewed < 10) {
widgets.status.innerHTML = urls.length+' Remaining'
}
else {
widgets.status.innerHTML = urls.length+' (Waiting)'
}
document.title = `(${unviewed}/${urls.length}) ${baseTitle}`
}
}
mainLoop = function() {
if (urls.length > 0 && !paused && loading < 2) {
if (unviewed < 5) {
mainItem(urls.shift())
}
}
updateUI()
}
let onPauseClick = function() {
paused = !paused
if (paused) {
widgets.pause.innerHTML = 'Resume'
}
else {
widgets.pause.innerHTML = 'Pause'
}
}
let onDownloadClick = function(alt) {
// Begin
urls = extractPreviews(document)
if (alt) {
urls.reverse()
}
// $(window).off()
document.querySelector('body').innerHTML =
`<style id='__style'>
body {
text-align: center;
}
#__status {
font-size: 1.4em;
margin: 10px;
}
#__control {
position: fixed;
background: grey;
padding-right: 5px;
border-radius: 0px 0px 10px 0px;
z-index: 100;
}
#__control > * {
display: inline-block
}
#__output img, #__output video {
display: block;
position: relative;
height: calc(100vh - 25px);
width: 98vw;
object-fit: contain;
}
img.gif-loader::after {
position: absolute;
top: 50%;
left: 50%;
content: 'Click to play GIF';
}
#__output a {
margin-bottom: 5vh;
}
body * {
margin-left: auto;
margin-right: auto;
}
</style>
<div id='__container'>
<div id='__control'>
<button id='__pause'>Pause</button>
<div id='__status'></div>
</div>
<div id='__output'><div></div></div>
</div>`
widgets.control = document.getElementById('__control')
widgets.status = document.getElementById('__status')
widgets.pause = document.getElementById('__pause')
widgets.output = document.getElementById('__output')
widgets.pause.addEventListener('click', onPauseClick)
let videoFunction = (func) => {
return (...args) => {
if (currentImg && currentImg.firstChild && currentImg.firstChild.nodeName == 'VIDEO') {
func(currentImg.firstChild, ...args)
}
}
}
// Video helpers
let seek = videoFunction((video, mod) => {
video.currentTime = Math.min(Math.max(video.currentTime + mod, 0), video.duration)
})
let attenuate = videoFunction((video, mod) => {
video.volume = Math.min(Math.max(video.volume + mod, 0), 1)
if (video.volume > 0 && video.muted) {
video.muted = false
}
})
let playOrPause = () => {
if (!currentImg) {
return null
}
let node = currentImg.firstChild
if (node.nodeName == 'VIDEO') {
if (!node.playing) {
node.play()
node.playing = true
}
else {
node.pause()
node.playing = false
}
}
else {
node.click()
}
}
document.onkeydown = function(event) {
if (event.key == ' ') {
if (currentImg) {
// Pause focus
if (currentImg.firstChild && currentImg.firstChild.playing) {
playOrPause()
currentImg.playOnFocus = true
}
// Get next focus
let sibling = event.shiftKey ? currentImg.previousSibling : currentImg.nextSibling
if (sibling) {
sibling.scrollIntoView()
currentImg = sibling
// Resume previously paused focus
if (currentImg.playOnFocus == true) {
playOrPause()
playOnFocus = false
}
// Set as viewed for buffered loading
if (!sibling.viewed) {
sibling.viewed = true
unviewed--
}
}
}
event.preventDefault()
}
// Volume up
else if (event.key == 'W') {
attenuate(0.2)
}
// Volume down
else if (event.key == 'S') {
attenuate(-0.2)
}
// Play or pause
else if (event.key == 'w') {
playOrPause()
}
// Seek forward
else if (event.key == 'd') {
seek(5)
}
// Seek backward
else if (event.key == 'a') {
seek(-5)
}
}
setInterval(mainLoop, timerInterval)
}
let divHeader = document.querySelector('.sidebar')
widgets.download = document.createElement('div')
widgets.download.innerHTML = '<button>Load all</button>'
widgets.download.lastChild.addEventListener('click', onDownloadClick, false)
divHeader.parentNode.insertBefore(widgets.download, divHeader.nextSibling)
document.onkeydown = function(event) {
if (event.key == ' ' && event.ctrlKey) {
onDownloadClick(event.altKey)
}
else if (event.key == ' ') {
let link = null
// Previous page at top
if (event.shiftKey && window.scrollY == 0) {
link = document.querySelector('a.prev_page')
}
// Next page at bottom
else if (!event.shiftKey && (window.innerHeight + window.scrollY) >= document.body.offsetHeight-100) {
link = document.querySelector('a.next_page')
}
if (link) {
link.click()
}
}
}
PostTweaks
Also works on r34
// ==UserScript==
// @name e6+ tweaks
// @namespace e6pt
// @include https://e621.net/post*
// @include https://rule34.xxx/index.php?page=post*
// @grant none
// ==/UserScript==
let img = document.getElementById('image')
let webm = document.getElementById('webm-container') // e621
|| document.getElementById('gelcomVideoPlayer') // rule34
let e = webm || img
// Video helpers
let seek = (video, mod) => {
video.currentTime = Math.min(Math.max(video.currentTime + mod, 0), video.duration)
}
let attenuate = (video, mod) => {
video.volume = Math.min(Math.max(video.volume + mod, 0), 1)
}
// Pause/play/seek keyboard controls
let playing = false
let onKeyDown = event => {
if (event.key == 'w') {
e.scrollIntoView()
}
if (webm) {
webm = document.getElementById('webm-container') // e621
|| document.getElementById('gelcomVideoPlayer') // rule34 reloads theirs?
// Volume up
if (event.key == 'W') {
if (webm.muted) {
webm.muted = false
webm.volume = 0
}
attenuate(webm, 0.2)
}
// Volume down
else if (event.key == 'S') {
attenuate(webm, -0.2)
}
else if (event.key == 'w') {
if (event.shiftKey) {
}
// Play or pause
else {
if (!playing) {
webm.play()
playing = true
}
else {
webm.pause()
playing = false
}
}
}
// Seek forward
else if (event.key == 'd') {
seek(webm, 5)
}
// Seek backwards
else if (event.key == 'a') {
seek(webm, -5)
}
// Mute toggle
else if (event.key == 'e') {
webm.muted = !webm.muted
}
}
}
document.onkeydown = onKeyDown
// Focus content
if (e) {
e.scrollIntoView()
setTimeout(() => {
e.scrollIntoView()
document.onkeydown = onKeyDown
}, 3000)
}
// Mute video
if (webm) {
webm.muted = true
}