Sleazy Fork is available in English.

Eza's Gallery Swallower

Turn a page of thumbnails into high-res images

Versão de: 19/02/2021. Veja: a última versão.

// ==UserScript==
// @name        Eza's Gallery Swallower
// @namespace   https://inkbunny.net/ezalias
// @description Turn a page of thumbnails into high-res images
// @license     MIT
// @license     Public domain / no rights reserved
// @include     https://www.pixiv.net/en/users/*
// @include     https://www.pixiv.net/bookmark_new_illust.php*
// @include     https://www.pixiv.net/en/tags/*/artworks*
// @include     https://www.pixiv.net/user/*/series/*
// @include     https://gelbooru.com/index.php?page*
// @include     https://e621.net/posts*
// @include     https://e926.net/posts*
// @include     https://*.booru.org/*s=list*
// @include     http://www.hentai-foundry.com/pictures/user/*
// @include     https://baraag.net/@*
// @exclude    *#dnr
// @noframes
// @version     2.6.14
// @grant        GM_registerMenuCommand
// ==/UserScript==



// Create a vertical view for high-res versions of all the image links you can see. 
// This includes all pages from multi-image submissions, where those exist.
// Individual images (or whole submissions) can be removed with one click. 
// Each image is also a link. 

// This script is no longer a mess. These notes, well, they're okay. 
// Oh, and the CSS is this way because I use a tallscreen monitor. Change img.short's max-height if that annoys you. 

// Scroll down past the wall of complaining to get to the code. 



// It might not even be possible to identify what error code onError caught. Jesus Christ, HTML. 
// Fetch still doesn't work on Pixiv. 
	// fetch( 'https://i.pximg.net/c/250x250_80_a2/img-master/img/2020/05/23/10/59/10/81782474_p0_square1200.jpg', { credentials: 'include' } ).then(response => response.json()).then(data => console.log(data)); - returns an empty object, but never, ever logs anything. Without credentials there's at least a CORS error. 
	// I literally just want to see the response. It should not be possible to not get that information - 'fine' is 200, 'fuck off' is 403, and even no response is 500. 
	// Oh for fuck's sake. I just tested it in mainstream Firefox to see if it was my old browser being shitty - FF still throws a CORS error.
	// It's i.pximg.net from pixiv.net. Aaaugh. 
	// I just want to know if a fucking image file exists! This is not an attack vector - you can do <img src onerror> and find the same damn information. Domain-agnostic. 
	// Do old-school  XMLHttpRequest()s work? I only want the head. Ugh, probably still stopped by CORS. 
	// Javascript: the language where reading a file is prohibited for safety, but executing it as a script is no-questions-asked. 
	// ... can we just run it as a script and see if it 404s? Obviously it won't work if it exists. 

// At this point I just need to style=visibility:hidden when things scroll offscreen. Same size, and god willing, no VRAM use. 
	// Maybe an interval that does this.src=this.src for all img elements - to fight when things half-load? Can't exactly press F5. 
	// Array.from( document.getElementsByTagName( 'img' ) ).forEach( i => i.src = i.src ) - inconsistently effective. 
	// I could add ?random-number to force a reload, but that's a complete re-download, so it's inadvisable. Can I check if it failed to-- oh right, onError. 
	// But I need to know what the error is, since we're still removing 404'd img elements with bad file extensions. 

// I should maybe move or copy the thumbnails into a top bit, like Pixiv Fixiv. 
	// Also, like Pixiv Fixiv, I should probably have the ability to repopulate existing sub-spans with images, 
		// both for "augh all PNGs slow loading" and "ah fuck loading broke." 

// What I have so far could easily replace Pixiv Fixiv's current code. The spans with this.parentElement.remove() stuff, anyway; we know the image count beforehand. 
	// For both, I'd like to stretch a thumbnail behind an image as it's loading. I'd need non-square thumbs. Or at least I'd really prefer them. 
	// I guess style=backgroundWhatever:that.src? I keep adding and removing this -> that for some damn reason. 

// Change the BG color to something dark. (Ego says #324.) 
// Testing whether loading shenanigans work: just make images visible on mouseover. 
// This accidentally ignores muted images - I'm calling that a feature. 
	// Ugoira animations get a spot, but don't load an image. Semi-feature. Deserves better handling and indication. 
// Clunky solution to memory problems: when an image loads, remove it, and put its URL in a list. 

// This script suggests a generic solution to Twitter nonsense: replace tweet divs with their media. Break classes and IDs so their infinite-scrolling JS can't remove them. No worries about their stupid random class names, since we'd just parent.parent.parent and then .replace(). 
	// Also I could probably just .remove() the sidebar bullshit. 

// Relying on onError suuucks. I categorically NEED a way to distinguish 'didn't finish loading' from 'file not found.' 
	// I can maybe get around this by assuming all the thumbnails load. They're tiny and quick. 
	// So if a thumbnail removes itself and produces a div where one file extension is expected to load, 
		// and that element has no children, we can infer the load failed, and start over with the three separate file extensions. 
	// Maybe ctrl+z instead of a per-image 're-show' button? Stick per-image-group IDs on a list, first in last out, as they're removed. 
	// Still might need per-submission do-over buttons for when loading is borked. Still want them, anyway. 
	// Be positive: have an onLoad for each image format. Signal success, not implicit failure. 
		// Or I guess do both: onError, check for success? 
		// Even just onError not removing an image if the other two are already gone would safely assume it timed out while the others 404'd. 

// Getting decent. Clone Pixiv Fixiv scaling modes, via changing image class and body class. Maybe go for unicode symbols instead of words. 
	// E.g. <-->, the vertical version of that, the cross version... '1:1' could just be text. Point is: not English. 
	// I can probably also do one-page-onscreen instead of as-it-comes vertical spacing, since I have divs around each image. 
// And use a damn dark mode already! 
// Buttons for forward/back? Floating over top, maybe. scrollTo stuff. Ech, but it has to update as you manually scroll down, so prev/next are at least consistently relative. 
	// Ideally the focus is somewhere in the middle of the screen, not like, one scanline of an image counts as being 'on' that image. 
// Per-image removal buttons could be larger and lower-contrast. Big easy target. Ignorable. 
// Spinner widget for individual images loading? I.e., keep spinning while the page is unfinished. That might be as simple as a DOM check. 
	// body.onload happens repeatedly, yeah? 
	// Style: thinking slow rotation, lower contrast, possibly inside the other spinner. 
	// I probably have to set/reset body.onError every time a new big-image starts loading. 
	// onstalled? onwaiting? onshow, for other things. Is oninvalid different from onerror? 
// Count beside spinners? 'X images, N loading.' 
// I could trivially turn this into a Pixiv Fixiv replacement - right? All I need is any image URL. No JSON shenanigans. 
// A stand-in for images that haven't loaded would be nice. Maybe their thumbnail, in thumbnail size. Just tell me when the top manga will affect scrolling. 
// Killing the submission reeeally needs to kill the underlying images. Maybe add another sub-sub-sub-span for next_image to target, and remove that. Then reloading the submission recreates that span before calling next_image on p0. 
// Come on, I can hack together previous / next page links. Only at the bottom. Doesn't trigger automatically. Just slap it in there. 
	// https://www.pixiv.net/en/users/1346633/artworks?p=2 -> p=1 and p=3. Needs to handle bare /artworks to infer ?p=1. 
	// Add buttons for scrolling to prev/next image/submission. Left-aligned, top of each image. 
// Long-term issue: this looks boring on a widescreen monitor. It's all tallscreen settings. Massive negative space otherwise. 
// Needs a big reload-all button at the top. 
// If I'm being clunky, I can probably grab image count from the counters in the corners. 
// Kinda want a "remove all first images" button. So many censored thumbnails. Japan - fuck off already. We're sorry we colonialized you. Stop ruining porn. 
// Ooh, Array.find(). Pass it a test function and it'll return the first element that matches. 

// This could trivially work on Baraag. (And other Mastodon sites.) 
// Oh, file-extension issues in Universal Scraper don't apply here. 
	// e621. 
	// Gelbooru:
		// https://thumbs.gelbooru.com/0e/dd/thumbnail_0edd1b5e5d7a1ba2488a135d60d70c78.jpg
		// https://img2.gelbooru.com//images/0e/dd/0edd1b5e5d7a1ba2488a135d60d70c78.jpg

// Image Toolbox on Pixiv shows the toolbox centered... with text... at the top of the screen. What is even the fuck. 
// This goes beyond "CSS is bullshit." This is getting into "CSS is haunted." 
// Removed the style making all spans position:relative, and that fixed the position, but there's still text. Only on Pixiv's bookmarks page. Why. 

// Gelbooru is inconsistent about WebMs. Some resolve to images. Others are blank. Dunno which is better, but pick one. 
	// https://gelbooru.com/index.php?page=post&s=list&tags=rosiekawaii 
// Gelbooru is inconsistent about images as well. https://gelbooru.com/index.php?page=post&s=view&id=5884654#dnr#&dnr 
	// https://gelbooru.com/index.php?page=post&s=list&tags=batako1812+manyuu_chifusa
	// https://img3.gelbooru.com//images/a5/de/a5de992f2e7cd27a79ffdc35e9ece3e1.jpeg
	// Oh, right, JPEG. Duh. I even anticipated that. 
// Still, address videos on Gelbooru. E.g. https://gelbooru.com/index.php?page=post&s=view&id=5614716&tags=rosiekawaii#dnr#&dnr
	// https://img3.gelbooru.com/images/3c/23/3c2376210444f7a7da737b412c722faa.jpg
	// https://img3.gelbooru.com//images/3c/23/3c2376210444f7a7da737b412c722faa.webm
	// Totally doable. So how do I check for a file without just embedding videos? Ugh, might be beyond onload / onerror behavior. 
	// Orrrr I could check tags. Not reliable. Could be wrong. But would allow a <video> with onerror to fall back to images. 
	// Better idea: indicate that it's probably a video, and link to the page without #dnr&dnr. Use Eza's Image Glutton, folks. 
		// Obviously that's the Ugoira solution on Pixiv: "don't." So it should be how I handle Ugoiras now that they're broken-ish. 
	// Embedding videos is undesirable because it implies linking to them for download. 
		// This script is already rude on bandwidth - videos would make it a DDOS attack. 
	// Solution: show thumbnail, not linked. (To avoid DownThemAll grabbing the thumbnail.) 

// FurAffinity? 
	// https://t.furaffinity.net/40652351@300-1613453026.jpg
	// https://d.furaffinity.net/art/ratcha/1613453026/1613453026.ratcha_party.jpg
	// Nope, needs a fetch. 

// CSS-only? Absurd tangent, but GreasyFork now lists CSS userscripts. It's probably possible to replace thumbnails on gallery websites with RegEx'd full-size versions, and fallbacks for each file extension. And obviously I think you can insert a line-break ::after each image and make them full-size or max-width / max-height. But yeesh. 

// Visions of replacing filter-filter-filter chains with querySelectorAll fell through because A>B[C] always grabs B... not A. 
	// let things = Array.from( document.querySelectorAll( 'a > img[src*="thumbs"]' ) ); 
	// You can get all images contained by a link. You cannot get all links containing an image. 
	// ... though you could get the images and do .parentElement a bunch. 
	// .closest( 'a' ) seeeeems to work. I do not know if it prefers hierarchy or in which direction. 
	// https://developer.mozilla.org/en-US/docs/Web/API/Element/closest 
	// Oh: it is explicitly going up the chain. Kickass, perfect. 

// Might use thumbnails of first image as scroll-into-view links at the top of the page. 
	// Maybe with Xs on each to remove things from there as well? Questionable. 

// Probing is possible without thumbnails - just onLoad( classList add "ready" ). Same results, but slower. Meh. 



	// Genuine bugs:

// https://www.pixiv.net/user/55117629/series/87453?p=2 - gets everything but the manga. Different page format. 

// Ugoira submissions display a big floating X, because those belong to the image-extension-trying block, not to each self-removing image. 
	// Ahh, it's because next_image would try _p0 and fail. This rewrite assumes the first image works. Maybe do cleanup in the interval?

// Dead images and possibly thumbnails take up horizontal space and cause slight movements while loading. 

// Spinners don't work on Baraag. 
	// And how could they? We never update per-element style. Even if @animation worked, it'd be stuck there. 



// Changes since last version:
	// Fixed Baraag.net previous / next controls. (PEBKAC.) 




// ------------------------------------ Custom replacement HTML ------------------------------------ //





// Replacement page. Not used immediately; it just makes more sense up here. 
var html = '';
var style_rules = new Object; 			// CSS "selector": "style" map. Blame Baraag. 



	// ----- //			CSS



// CSS rules as an associative array, so they can be applied per-element on uncooperative sites. 

// Image style(s):
style_rules[ "img.short" ] = "max-width: 90vw; max-height: 60vh; z-index: 10; vertical-align: middle;"; 

// Dead spinners:
style_rules[ '.spacer' ] = 'position: absolute; width: 0px; height: 0px;'; 
style_rules[ '.other_spacer' ] = 'position: absolute; width: 0px; height: 0px;'; 	/* Arbitrarily smaller. */
style_rules[ '#image_counter' ] = 'position: absolute; left: 70px; top: 5px; font-size: 33px;';

// Controls: 
style_rules[ '.remover' ] = 'background-color:#d7dbd8; border-radius: 50%; width: 60px; height: 60px; text-align: center; display: inline-block; border:1px solid #ab1919; cursor:pointer; line-height: 20px; color:#4d1919; font-family:Arial; font-size:17px; padding: 10px 10px; text-decoration:none;';
style_rules[ '.remover:hover' ] = 'background-color:#bd2a2a;';
style_rules[ '.floating' ] = 'z-index: 1; position: absolute; top: 50%; -ms-transform: translateY(-50%); transform: translateY(-50%); width: 120px; height: 120px; font-size:45px;';
style_rules[ '.reloader' ] = 'background-color:#dbd7d8; border-radius: 50%; width: 60px; height: 60px; text-align: center; display: inline-block; border:1px solid #19ab19; cursor:pointer; line-height: 20px; color:#194d19; font-family:Arial; font-size:33px; padding: 10px 10px; text-decoration:none;';
style_rules[ '.reloader:hover' ] = 'background-color:#2abd2a;';

// Hidden thumbnails, probing to check for next image in multi-image sets:
style_rules[ '.test, .thumb' ] = 'display:none'; 

// Push all of that into a <style> block:
html += '<style> ';
for( selector in style_rules ) { 
	html += selector + ' { ' + style_rules[ selector ] + ' } \n'; 
} 
html += '</style> ';

// Spinners:
html += '<style> .submissions_loader { position: absolute; left: 0px; top: 0px; border: 8px solid #3498db; border-top: 8px solid #111111; border-bottom: 8px solid #111111; border-radius: 50%; width: 48px; height: 48px; animation: spin 1s linear infinite; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } </style>'; 		
html += '<style> .images_loader { position: absolute; left: 8px; top: 8px; z-index: -1; border: 24px solid #db9834; border-top: 24px solid #aaaaaa; border-bottom: 24px solid #AAAAAA; border-radius: 50%; width: 0px; height: 0px; animation: images_spin 3s linear infinite; } @keyframes images_spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } </style>';  
// Either I bodged the implementation of style_rules to a <style> block, or there's some other obstacle to animations. So these are separate. 




	// ----- //			Page elements



html += '<div class="submissions_loader" id="submissions_spinner"></div>' 		/* Spinner for submissions, 'new images being found.' */
html += '<div class="images_loader" id="images_spinner"></div>' 		/* Spinner for images, 'images loading in high-res.' */
html += '<span id="image_counter"></span>'; 		/* This detaches. */

html += '<br><br><center><span id="centered"></span></center>';  		/* Where stuff goes. */
html += '<br><br><br>'; 		/* Spacing for prev/next links. */
html += '<center><span id="links"></span></center>'
html += '<br><br><br><br><br>'; 		/* Runout. */

// Additional HTML may be added by per-site functions. Once injected, we use the DOM. 





// ------------------------------------ General setup & Per-site functions ------------------------------------ //





	// ----- //			Global variables 



var trigger_size = [ 15, 70, 25, 10 ]; 		// Left, top, font-size, padding. Pixiv defaults. 
var button_delay_function = false; 			// Should maybe have separate boolean and function. 
var proper_fetch = false; 			// If true, we fetch pages instead of modifying thumbnail URLs. (Not yet implemented.) 
var top_to_bottom = false; 		// Chronological order is confusing to describe. 
var automatic_pagination = false; 		// Pixiv-style p0, p1, p2, etc. 
var gather_items;  		// Per-site function to scrape page contents. 
var items;  					// Scraped contents of page. 
var formats = [ ".png", ".jpg", ".gif", ".jpeg" ]; 		// File extensions. (Edit: no sense not having JPEG by default.)
var page_number = 1; 				// Default.
var next_page, previous_page;  	// URLs of obvious purpose. 
var thumb_followup = '';  		// Used to be part of "items," per-item, but only Pixiv uses them, and they're standard. 
var image_followup = ''; 		// Ditto. 
var new_image_rate = 200; 		// Milliseconds between checks for "ready" submissions. Lower is faster. 
var force_style = false; 			// If sites don't allow inline CSS, apply style_rules to each element. 




	// ----- //			Helper functions 



// Turn window.location.search into a sensible associative array. Should be standard, guys!
function parse_search( search_string ) { 
	let strings = search_string.split( /[\?\&]/ )
		.filter( v => v ) 		// Remove empty strings
	let associative = new Object;
	strings.forEach( v => associative[ v.split('=')[0] ] = v.split('=')[1] ); 
	return associative; 
}

function scrub_extensions( url, format_list ) { 
	if( format_list == null ) { format_list = formats; } 		// Shut up, it's global. 
	format_list.forEach( ext => 
		url = url.replace( ext, '' ) )
	return url; 
} 



	// ----- //			Per-site setup and gather functions 



var args = parse_search( window.location.search ); 		// DRY 

switch( document.domain.replace( 'www.', '' ) ) {

	case 'pixiv.net':
		thumb_followup = '_square1200'; 
		formats = [ ".png", ".jpg", ".gif" ]; 	// I have never seen a JPEG on Pixiv. I have 100,000 _p0 JPGs, and they're all ".jpg". 
		automatic_pagination = true; 

		// Ever-useful test profile: https://www.pixiv.net/en/users/53625793 
		button_delay_function = () => 
			document.querySelector( 'img[src*="250x"]' ) ||
			document.querySelector( 'a *[style*="240x"]' ); 

		// https://www.pixiv.net/bookmark_new_illust.php?p=2
		if( args["p"] ) { page_number = parseInt( args["p"] ); } 
		next_page = window.location.origin + window.location.pathname + "?p=" + (page_number + 1)
		previous_page = window.location.origin + window.location.pathname + "?p=" + (page_number - 1); 
		// And now per-image removers are at the bottom again. because CSS is punishment for mankind's hubris. 

		gather_items = function() { 
			// Not sure the Array-map approach clarified or simplified anything, in Pixiv's case...
			return Array.from( document.querySelectorAll( 'a[href*="/art"] img[src*="c/2"]' )  )
				.map( v => v.closest( 'a' ) )
				// https://www.pixiv.net/en/users/53625793
				.map( v => new Object( { 
					page: 		v.href,
					title: 			v.href.split('/').pop(),
					// https://i.pximg.net/c/250x250_80_a2/img-master/img/2020/06/22/05/58/05/82488314_p0_square1200.jpg
					// https://i.pximg.net/img-original/img/2020/06/22/05/58/05/82488314_p0.jpg
					thumb: 	scrub_extensions( v.querySelector( 'img' ).src )
						.replace( '_custom1200', '' )
						.replace( '_master1200', '' )
						.replace( '_square1200', '' )
						.replace( '_p0', '_p' ),
					image: 		scrub_extensions( v.querySelector( 'img' ).src )
						.replace( '_custom1200', '' )
						.replace( '_master1200', '' )
						.replace( '_square1200', '' )
						.replace( '_p0', '_p' )
						.replace( 'c/250x250_80_a2/', '' )
						.replace( 'c/240x240/', '' )
						.replace( 'img-master/', 'img-original/' )
						.replace( 'custom-thumb/', 'img-original/' ) 		// Some of these are redundant but it's not worth figuring out which. 
				} ) )

			.concat( Array.from( document.querySelectorAll( 'a *[style*="background-image"]' ) )
				.map( v => v.closest( 'a' ) )
				// https://www.pixiv.net/bookmark_new_illust.php?p=2 
				.map( v => new Object( { 
					page: 		v.href,
					title: 			v.href.split('/').pop(),
					// url("https://i.pximg.net/c/240x240/img-master/img/2021/02/17/04/05/41/87838098_p0_master1200.jpg")
					// https://i.pximg.net/img-original/img/2021/02/17/04/05/41/87838098_p0.jpg
					thumb: 	scrub_extensions( v.querySelector( 'div[style]' ).style.backgroundImage )
						.replace( 'url("', '' )
						.replace( '")', '' ) 
						.replace( '_custom1200', '' )
						.replace( '_master1200', '' )
						.replace( '_square1200', '' )
						.replace( '_p0', '_p' ),
					image: 		scrub_extensions( v.querySelector( 'div[style]' ).style.backgroundImage )
						.replace( 'url("', '' )
						.replace( '")', '' )  
						.replace( '_custom1200', '' )
						.replace( '_master1200', '' )
						.replace( '_square1200', '' )
						.replace( '_p0', '_p' )
						.replace( 'c/250x250_80_a2/', '' )
						.replace( 'c/240x240/', '' )
						.replace( 'img-master/', 'img-original/' )
						.replace( 'custom-thumb/', 'img-original/' ) 
				} ) )
			)

/*
			let things = Array.from( links ) 		// There's probably a less-verbose querySelectorAll for this.
				.filter( a => a.href )
				.filter( a => a.href.indexOf( '/artworks/' ) > 0 )
				.filter( a => a.querySelector( 'img' ) )
				.filter( a => a.querySelector( 'img' ).src )
				.filter( a => a.querySelector( 'img' ).src.match( 'c/2' ) );
			// document.querySelectorAll( 'a[href*="/art"] img[src*="c/2"]' ) 
			things = things.concat( Array.from( links ) 		// Sort and reverse: side-effectful. Concat: functional. Go fuck yourself, Javascript. 
				.filter( a => a.innerHTML.match( 'background-image' ) )
			) 
			// document.querySelectorAll( 'a *[style*="background-image"]' )
			let items = new Array;
			things.forEach( (v,i,a) => { 
				let item = new Object;
				item.page = v.href; 
				item.title = v.href.split('/').pop(); 		// Just the number - large print. 

				// https://i.pximg.net/c/250x250_80_a2/img-master/img/2020/06/22/05/58/05/82488314_p0_square1200.jpg
				// https://i.pximg.net/img-original/img/2020/06/22/05/58/05/82488314_p0.jpg
				if( v.querySelector( 'img' ) ) { 	// Would it kill you to return null for all properties of null? Does it have to be a TypeError? 
					item.thumb = v.querySelector( 'img' ).src
				} else { 
					item.thumb = v.querySelector( 'div[style]' ).style.backgroundImage
						.replace( 'url("', '' )
						.replace( '")', '' ); 
				}
				item.thumb = scrub_extensions( item.thumb )
				item.thumb = item.thumb
					.replace( '_p0', '_p' );   		// Remember to add a page number. 
				item.thumb = item.thumb
					.replace( '_custom1200', '' )
					.replace( '_master1200', '' )
					.replace( '_square1200', '' ); 
				item.image = item.thumb
					.replace( 'c/250x250_80_a2/', '' )
					.replace( 'c/240x240/', '' )
					.replace( 'img-master/', 'img-original/' )
					.replace( 'custom-thumb/', 'img-original/' );		// And still remember to add a page number. 

				items.push( item ); 
			} ); 
			return items; 
*/
		}
	break; 

	case 'gelbooru.com':
		// https://gelbooru.com/index.php?page=post&s=list&tags=shuujin_academy_uniform+chair+1girl+pink_background 
		trigger_size = [ 15, 35, 16, 5 ]; 		// Left, top, font-size, padding. 

		// https://gelbooru.com/index.php?page=post&s=list&tags=4girls
		// https://gelbooru.com/index.php?page=post&s=list&tags=4girls&pid=42
		// https://gelbooru.com/index.php?page=comment&s=list&pid=10 		// Whoops. 
		page_number = 0; 
		let pages_at_once = 42; 
		if( window.location.href.match( 'page=comment' ) ) { pages_at_once = 10; } 
		if( args["pid"] ) { page_number = parseInt( args["pid"] ); } 

		next_page = window.location.href + "&pid=" + (page_number + pages_at_once); 		// Flawed, but it works. 
		previous_page = window.location.href + "&pid=" + (page_number - pages_at_once); 

		gather_items = function() { 

			return Array.from( document.querySelectorAll( 'a[href*="s=view"]' ) )
				.map( v => new Object( { 
					// https://gelbooru.com/index.php?page=post&s=view&id=4179699&tags=pink_background
					page: 		v.href,
					title: 			v.href.split( '&id=' )[1].split( '&' )[0],
					// https://img3.gelbooru.com/thumbnails/f5/c7/thumbnail_f5c7826072943fd72076ba9121b473f0.jpg
					// https://img3.gelbooru.com/images/f5/c7/f5c7826072943fd72076ba9121b473f0.jpg
					thumb: 	scrub_extensions( v.querySelector( 'img' ).src ),
					image: 		scrub_extensions( v.querySelector( 'img' ).src )
						.replace( '/thumbnails', '/images' )
						.replace( 'thumbnail_', '' ) 
				} ) )

		}
	break;

	// Janky e621 issues seem to be because JS isn't allowed to run on the page? Somehow? It's not ScriptBlock. 
		// "The page’s settings blocked the loading of a resource at self." 
		// Aaaargh there's probably some horrible version of this where I fake on_error and on_load inside this script's interval function. 
	// Intermittent issue: just had e621's images all disappear. Second time this has happened. Not sure the first time was on this site. 
		// Both times, the page had been sitting open (second monitor) for like a minute. Fresh reload before that. Console almost certainly open.
		// img elements still show valid src. 
		// .post-preview, #image-container, #c-comments .post, .mod-queue-preview.post-preview, .post-thumbnail { visibility: hidden !important; }
		// ... so why did that get set? It's triggering on .post-preview. That's at the <article> level... for some reason. 
		// This code only touches article elements to check for blacklisting, it only does that if gather_items is called. Which it wasn't. 
	case 'e621.net':
	case 'e926.net':
		// https://e621.net/posts?tags=somik+mirror
		trigger_size = [ 15, 50, 16, 5 ]; 		// Left, top, font-size, padding. 

		// https://e621.net/posts
		// https://e621.net/posts?page=2
		// https://e621.net/posts?tags=asthexiancal++
		// https://e621.net/posts?page=2&tags=asthexiancal++ 
		if( args["page"] ) { page_number = parseInt( args["page"] ); } 
		next_page = window.location.href + "&page=" + (page_number + 1); 		// Insufficient on https://e621.net/posts
		if( next_page.indexOf( '?' ) < 0 ) { next_page = next_page.replace( '&', '?&' ); } 		// Klunk. 
		previous_page = window.location.href + "&page=" + (page_number - 1); 

		gather_items = function() { 

			return Array.from( document.querySelectorAll( 'a[href*="posts/"] img' ) )
				.map( v => v.closest('a') )
				.filter( v => ! v.closest('article').className.match( 'blacklisted-active' ) ) 		// Exclude hidden posts. 
				.map( v => new Object( { 
					// https://e621.net/posts/1333873?q=somik+mirror
					page: 		v.href,
					title: 			v.href.split( 'posts/' )[1].split( '?q=' )[0],
					// https://static1.e621.net/data/preview/37/75/3775cd8664c688f98a41780f6796ce86.jpg
					// https://static1.e621.net/data/37/75/3775cd8664c688f98a41780f6796ce86.png
					thumb: 	scrub_extensions( v.querySelector( 'img' ).src ),
					image: 		scrub_extensions( v.querySelector( 'img' ).src )
						.replace( '/preview', '' )
				} ) )

		}
	break;

	case 'hentai-foundry.com':
		var trigger_size = [ 15, 20, 25, 10 ]; 

		// button_delay_function might be the only way to separate gallery pages from submission pages. HF's URLs are duuumb. 

		// http://www.hentai-foundry.com/pictures/user/AmaZima/page/9
		if( window.location.href.indexOf( '/page/' ) > 0 ) { 
			page_number = parseInt( window.location.href.split( '/' ).pop() ); 
			previous_page = window.location.href.split( '/page' )[0] + '/page/' + (page_number - 1); 
		} 
		next_page = window.location.href.split( '/page' )[0] + '/page/' + (page_number + 1);  	// page_number defaults to 1. 

		gather_items = function() { 

			return Array.from( document.querySelectorAll( 'a.thumbLink' ) ) 
				.map( v => {
						// http://www.hentai-foundry.com/pictures/user/AmaZima/589016/Tired-but-happy-Lottie
						let username = v.href.split( '/' )[5]; 	// AmaZima
						let title = v.href.split( '/' )[6]; 			// 589016
						return new Object( { 
							page: 		v.href,
							title: 			title, 
							// url("//thumbs.hentai-foundry.com/thumb.php?pid=589016&size=350")
							// http://pictures.hentai-foundry.com/a/AmaZima/589016/AmaZima-589016-Tired_but_happy_Lottie.png
							// http://pictures.hentai-foundry.com/t/Tixnen/869342/Tixnen-869342-Vasilina.jpg
							thumb: 	scrub_extensions( v.querySelector( 'span[style]' ).style.backgroundImage )		// Why would it be easy.
								.replace( 'url("', '' )
								.replace( '")', '' ),
							image: 		window.location.protocol + "//pictures.hentai-foundry.com/" 
								+ username.slice( 0, 1 ).toLowerCase() + '/' + username + '/'
								+ title + '/' 
								+ username + '-' + title + '-'
								+ v.href.split( '/' ).pop().replace( /-/g, '_' )
						} )
				} )

		} 
	break; 

	// Baraag... and Mastodon in general, ideally. 
	// Oof. Baraag has multiple submissions per post, but they're not numerically related.
	// JS answer: let items.image be an array. Check typeOf and either show one or all. 
		// Better JS answer: check typeOf and show one at a time, like Pixiv. (Easier to handle individual Xs that way anyhow.) 
		// Bonus for Baraag, we can set formats to [""] and just show the correct image.
		// Also maybe delay when "ready" again, so we don't instantly re-ready this one submission. Avoid linearity. 
		// We don't need a thumbnail value. Buuut we might have one anyway, if we implement Pixiv Fixiv style shortcuts at the top. 
	// Instead of typeOf - item.thumbs vs item.thumb? Neither is especially elegant. Global boolean?
		// I am hard pressed not to just lean on typeof. Why keep track? If you pass an array of URLs, you get an array of images. 
		// Out-of-bounds signalling for page count -might- still be useful for Pixiv. That'd let us skip the thumbnail-try stuff. 
	// Argh, retweets ("boosts?") aren't of class h-entry. They're h-cite. Can I just querySelector with a comma? Sure can. 
		// Guess that puts a nail in whether to grab links and filter down, or one-shot with complex CSS and work up. 
	case 'baraag.net':
		formats = [""]
		force_style = true; 		// querySelectorAll, element.style = be round, dammit. 

		// https://baraag.net/@Applalt/media
		// https://baraag.net/@Applalt/media?max_id=105165058865018699 // Yeesh. 
		let navs = document.querySelectorAll( 'a[class*="load-more"]' ); 
		if( navs ) { 
			if( navs.length == 2 ) { previous_page = navs[0].href; next_page = navs[1].href; }  		// Both
			else if( window.location.href.indexOf( '?max_id=' ) > 0 ) { previous_page = navs[0].href; } 		// Last page
			else { next_page = navs[0].href; } 		// First page
		} 		// There is no concept of a page number. If previous_link is undefined, it simply won't display. 

		gather_items = function() { 

	 		// Is it better to get links and filter by children, or select precisely and map to ancestors? 
			// Certainly it's clearer to get links and filter. Dunno if performance matters. 
//			return Array.from( document.querySelectorAll( '.h-entry div[data-props*="media"]' ) ) 
//				.map( v => v.closest( '.h-entry' ) ) 
			return Array.from( document.querySelectorAll( '.h-entry, .h-cite' ) ) 
				.filter( v => v.querySelector(  'div[data-props*="media"]' ) ) 		// Text-only posts fuck us up. 
				.map( v => {
						let item = new Object; 
						item.page = v.querySelector( 'a[class*="time"][href]' ).href;
						item.title = item.page.split( '/' ).pop(); 

						// dataset.props example: Object { height: 343, sensitive: true, autoplay: true, media: Array[3] }
						// .media example: Array [ Object, Object, Object ]
						// .media[0]: Object { id: "105651575500861937", type: "image", 
							// url: "https://baraag.net/system/media_att…", preview_url: "https://baraag.net/system/media_att…", 
							// remote_url: null, preview_remote_url: null, text_url: "https://baraag.net/media/C62aWhAqWt…", 
							// meta: Object, description: null, blurhash: "UFCGJfX99@RP^%t7OoV@4ms:kpn+x[V@Rja#" }
						// ... but none of those URLs are the post URL, so keep original item.page code. 
						let data = JSON.parse( v
								.querySelector( 'div[data-props]' )
								.dataset.props )
							.media; 		// Array of objects

						item.thumb = data.map( v => v.preview_url ); 	// Arguably should be a single image? One thumb per submission? 
						item.image = data.map( v => v.url ); 

						return item; 
				} )

		} 
	break; 

}

// Subdomains always have to cause problems. 
if( document.domain.split( '.' ).slice( 1, 3 ).join( '.' ) == "booru.org" ) { 		// *://*.booru.org/* 
	trigger_size = [ 15, 55, 16, 5 ]; 		// Left, top, font-size, padding. 

	// https://svtfoe.booru.org/index.php?page=post&s=list&tags=crack_ship&pid=20
	// https://svtfoe.booru.org/index.php?page=post&s=list&tags=socks&pid=20
	page_number = 0; 
	if( args["pid"] ) { page_number = parseInt( args["pid"] ); } 
	next_page = window.location.href + "&pid=" + (page_number + 20); 		// Flawed, but it works. 
	previous_page = window.location.href + "&pid=" + (page_number - 20); 

	gather_items = function() { 

		return Array.from( document.querySelectorAll( 'a img[src*="thumbs"]' ) )
			.map( v => v.closest( 'a' ) )
			.filter( v => ! v.style.display ) 		// Exclude hidden posts. Should be !display=="none", but typeError says fuck you. 
			.map( v => new Object( { 
				// https://svtfoe.booru.org/index.php?page=post&s=view&id=29292
				page: 		v.href,
				title: 			v.href.split( '=' ).pop(), 
			 	// https://thumbs.booru.org/svtfoe/thumbnails//28/thumbnail_187209e3ca22a28fd1ce75a7fd4f54aee3cf1e62.jpg
				// https://img.booru.org/svtfoe//images/28/187209e3ca22a28fd1ce75a7fd4f54aee3cf1e62.jpg
				thumb: 	scrub_extensions( v.querySelector( 'img[src*="thumbs"]' ).src ), 
				image: 		scrub_extensions( v.querySelector( 'img[src*="thumbs"]' ).src ) 
					.replace( 'thumbs.', 'img.' )
					.replace( '/thumbnails', '/images' )
					.replace( 'thumbnail_', '' ) 
			} ) )

	} 
}



	// ----- //			Controls to invoke script



GM_registerMenuCommand( "Swallow entire gallery", show_images ); 		// GreaseMonkey dropped this feature years ago, but I am stubborn. 

// Put button on page, since there's no menu in "modern" Userscript plugins.
// Onclick, change class to some spinner, so it reacts instantly and looks like it's loading. Really the interval is waiting a second. 
var trigger = document.createElement( 'button' ); 
trigger.innerText = "Swallow gallery"; 
trigger.className = "unclicked_button"; 
trigger.onclick = function(){ this.innerText='Swallowing...'; this.className = 'clicked_button'; } 		// Immediate visible change, idempotent
trigger.style = "position: absolute; left: " + trigger_size[0] + "px; top: " + trigger_size[1] + "px; background-color:#dbd7d8; border-radius: 20px; text-align: center; display: inline-block; border:1px solid #19ab19; cursor:pointer; line-height: 20px; color:#194d19; font-family:Arial; font-size:" + trigger_size[2] + "px; padding: " + trigger_size[3] + "px " + trigger_size[3] + "px; text-decoration:none;"

// Pixiv doesn't work properly until after thumbnails load, so don't show the button until then. 
if( typeof( button_delay_function ) == "function" ) { 		// Note: checking for presence of function. 
	add_button = setInterval( function() { 
		if( button_delay_function() ) { 		// Note: function called, checking results. 
			clearInterval( add_button ); 
			document.body.appendChild( trigger ); 
		}  
	}, 1000 ); 		// Passive - no interaction concerns. 
} else { 
	document.body.appendChild( trigger ); 		// Most sites are ready-to-go. 
}

// Injecting code into the page is nontrivial - ironically because function.toString is fragile - so just look for a change in the page. 
var button_check = document.getElementsByClassName( 'clicked_button' ); 
var fake_event = setInterval( function() {  
		if( button_check.length > 0 ) { 
			clearInterval( fake_event ); 
			show_images(); 
		} 
	}, 400 );  		// Doherty threshold for frustration is 400ms. 

// function fetch_pages( page_links ) { }		// Still expecting to use this approach on some site, but so far, nnnope. 

// End of main execution. 





// ------------------------------------ Gallery Swallower ------------------------------------ //





function show_images() { 



	// ----- //			Replace page, set up furniture 



	// Grab links and/or thumbnails using per-site code:
	items = gather_items(); 		// Array of objects, listing page link, thumbnail, presumed fullsize image, etc. 

//	console.log( items ); 		// Debug. Be honest, this is staying here. 

	if( top_to_bottom == false ) { items.reverse(); } 

	// Erase existing page, use ours instead
//	document.head.innerHTML = ''; 		// Looks bad, accomplishes nothing. 
	document.body.innerHTML = html; 

	// Navigation links, at the bottom
	let links_element = document.getElementById( 'links' ); 		// Has to go after document = html, duh.

	// "Previous" or "Previous - Next" or "Next"
	let link_html = "";
	if( page_number <= 1 ) { previous_page = ''; } 		// No previous link on first pages. (Page 0 is valid.)
	if( previous_page ) { link_html += "<a href='" + encodeURI( previous_page ) + "'>Previous page</a>"; } 
	if( previous_page && next_page ) { link_html += " - "; }
	if( next_page ) { link_html += "<a href='" + encodeURI( next_page ) + "'>Next page</a>"; } 
	links_element.innerHTML = link_html; 

	let centered = document.getElementById( 'centered' ); 



	// ----- //			Per-submission links and controls 



	// Give each item its own set of spans, with basic onClick controls to remove images or reload a submission.
	for( let item_key = 0; item_key < items.length; item_key++ ) { 
		item = items[ item_key ]; 
		let container = document.createElement( 'span' ); 
		container.id = item_key + 'container'; 
		let basket = document.createElement( 'span' );		// Two hard problems. 
		basket.id = item_key; 

		let reloader = document.createElement( 'button' ); 
		reloader.innerText = '⟳'; 
		reloader.onclick = function() { 
			let e=document.getElementById( item_key ); 
			e.innerHTML = ""; 
			e.dataset.page_number=0; 
			e.className="ready"; 
		} 
		reloader.className = 'reloader'; 

		let link = document.createElement( 'a' ); 
		link.href = item.page + '#dnr#&dnr'; 
		link.innerText = ' ' + item.title + ' '; 
		link.setAttribute( "target", "_blank" ); 
		link.style = 'font-size:30px'; 

		let submission_remover = document.createElement( 'button' );  		// Erase whole submission. 
		submission_remover.innerText = '❌'; 
		submission_remover.onclick = function(){ document.getElementById( item_key ).innerHTML = ""; } 
		submission_remover.className = 'remover'; 		// Add some CSS to float left or whatever. 

		// Maybe don't do bottom remover on single-image sites. It's clutter.
			// Would arguably still be useful for very tall comic strips, except we don't have image-size controls yet. 
		let bottom_submission_remover = submission_remover.cloneNode(); 		// Why doesn't this include innerText? 
		bottom_submission_remover.innerText = '❌';  	
		// Scroll back up on removal, since a bunch of vertical content disappeared. 
		// The presumed use of this button is when you've scrolled past a long-ass manga and gone "meh," 
			// so you don't want to hunt for the root reload / remove buttons. That crap killed me in Tumblr Scrape. 
		bottom_submission_remover.onclick = function() {
			document.getElementById( item_key ).innerHTML = ""; 
			document.getElementById( item_key + 'container' ).scrollIntoView(); 
		} 

		container.appendChild( reloader ); 
		container.appendChild( link ); 
		container.appendChild( submission_remover ); 
		container.appendChild( document.createElement( 'br' ) ); 
		container.appendChild( basket ); 
		container.appendChild( bottom_submission_remover ); 
		container.appendChild( document.createElement( 'br' ) ); 

		centered.appendChild( container ); 

		if( force_style ) { 		// Apply CSS rules to every element individually. 
			for( selector in style_rules ) { 
				Array.from( document.querySelectorAll( selector ) )
					.forEach( element => { 		// Finding .cssText was so hard, you'd think it's a secret. 
						element.style = element.style.cssText + style_rules[ selector ]; 
					} )
			} 
		} 

		basket.dataset.page_number = 0; 
		basket.classList.add( "ready" ); 		// Signals interval function to load an image here. 
	}  



	// ----- //			Ongoing interaction and loading 



	// Load more images in "ready" submissions, when available. 
	// Sloppy speedup: "while ready_element = [0]." Service all ready spaces, every time. 
		// Cons: might 503, might lock up. 
		// Tried while( ready_element = ready_elements[0] ). Unscientific results: same time either way. Eh. 
	// Better kludge: forEach, setTimeout. It'll complete the for-each, then schedule each single-service action, in order. 
		// Cons: minor race condition where one "ready" state could get scheduled repeatedly. (If another interval occurs before that timeout.) 
		// Cons: still might 503. 
	var ready_elements = document.getElementsByClassName( 'ready' ); 
	var interval_object = setInterval( function() { 

			// Add an image to the ready basket, increment page number, conditionally ready-up for another image. 
			// Image list: just grab list[n]. Automatic pagination: probe for matching numbered thumbnail. 
			if( ready_element = ready_elements[0] ) { 		// "If ready_elements.length > 0," but with race condition paranoia. 
				ready_element.classList.remove( 'ready' ); 		// We fancy. 

				let item_id = parseInt( ready_element.id ); 
				let manga_page = parseInt( ready_element.dataset.page_number ); 		// Not web-page... comic-page. 
				ready_element.dataset.page_number = 1 + manga_page; 

				// Prepare filename, once:
				let image_url = items[ item_id ].image; 
				if( typeof( items[ item_id ].image ) == "object" )  		// typeof, not typeOf? "object", not "Array"? Fuck you, Javascript. 
					{ image_url = items[ item_id ].image[ manga_page ]; }
				if( automatic_pagination ) 		// Basically just Pixiv. 
					{ image_url +=  manga_page; } 
				image_url += image_followup; 		// Usually nothing. 

				// Try all plausible file extensions for an inline image. 
				let forms = new Array; 
				for( format of formats ) { 
					let png = document.createElement( 'img' );
					let apng = document.createElement( 'a' ); 
					apng.appendChild( png );

					png.className = "short loading"; 		// Debug-ish. Also, doesn't work for some goddamn reason. 
					png.src = image_url + format; 
//					console.log( image_url ); 
					png.onerror = function() { this.parentElement.remove(); } 		// Remove parent link (apng / ajpg / agif) as well. 
					png.onload = function() { this.classList.remove( "loading" ); }

					apng.href = png.src; 
					apng.setAttribute( "target", "_fuckgelbooru" ); 		// WHY IS THERE NO OPPOSITE TO DISPLAY:NONE?!

					forms.push( apng ); 
				}

				let remover = document.createElement( 'button' );  		// Big red X, to remove this image specifically. 
				remover.innerText = '❌'; 
				remover.onclick = function() { this.parentElement.remove(); } 
				remover.className = 'remover floating'; 

				// New span per-image, so whole-submission X prevents new images from loading:
				let outer_span = ready_element.querySelector( 'span' ); 
				if( outer_span == null ) { 
					outer_span = document.createElement( 'span' ); 
					ready_element.appendChild( outer_span ); 
				} 
				let inner_span = document.createElement( 'span' ); 
				inner_span.style = "position: relative;"; 		// This fixes the vertically-aligned Xs. Do NOT ask me how. 
				outer_span.appendChild( inner_span ); 

				for( form of forms ) { inner_span.appendChild( form ); } 		// Insert all image tries. 
				inner_span.appendChild( remover ); 		// Also insert delete-this button... afterward? Yeah, on the right-hand side. 
				inner_span.appendChild( document.createElement( 'br' ) ); 
				inner_span.appendChild( document.createElement( 'br' ) ); 



				// Thumb-try stuff:
				if( automatic_pagination ) { 

					forms = new Array; 
					for( format of formats ) { 
						png = document.createElement( 'img' ); 		// Reuse 
						png.src = items[ item_id ].thumb + (manga_page+1) + thumb_followup + format;
						png.className = 'test'; 
						png.onerror = function() { this.remove(); } 
						// Any truly hideous behavior is often attributable to the wrong number of .parentElements below: 
						png.onload = function() { this.parentElement.parentElement.parentElement.classList.add( "ready" ); this.remove(); }
						forms.push( png )
					} 

					for( form of forms ) { inner_span.appendChild( form ); } 
				}

				// Array-of-images stuff:
				if( typeof( items[ item_id ].image ) == "object" ) { 		// Not a global boolean, because direct testing beats keeping track. 
					if( manga_page + 1 < items[ item_id ].image.length ) { 
						ready_element.classList.add( "ready" ); 
					} 
				}

				// Force style, but only for children of this element. 
					// Should maybe only be for children of inner_span or outer_span? No; also applies to Pixiv thumbnail probes. 
					// ... though "+=" does mean 30-image submissions will have one element with style=size;size;size;size;, etc. 
					// Nevermind, text -> CSS2Properties eats all duplicates. 
				if( force_style ) { 
					for( selector in style_rules ) { 
						Array.from( outer_span.querySelectorAll( selector ) )
							.forEach( element => { 
								element.style = element.style.cssText + style_rules[ selector ]; 
							} )
					} 
				} 

			} 

		}, new_image_rate ); 



	// ----- // 		🍭 Spinners 🍭



	// Garish spinners that indicate "finding new images" and "files still loading." 
	// Easy answer for responsive loading / lazy spinners: use two intervals. Duh. 
	// I could simplify this to e.g. images_spinner.className = images_loading.length and do CSS bullshit like #id.class=spinning #id.0=not-spinning. 
		// I don't think there's any truly automatic way to spin based on the properties of children... but I wouldn't be surprised. 
		// Like I'm pretty sure per-element style properties can't go 'animate if( self.querySelector )'. But again: not ruling it out. 
		// It's only desirable for forced_style sites like Baraag, since lazy spinners are better, so pfffft. 
	var submissions_loading, submissions_spinner; 		// These are technically global? 
	var images_loading, images_spinner; 
	submissions_loading = document.getElementsByClassName( 'test' );
	submissions_spinner = document.getElementById( 'submissions_spinner' );
	images_loading = document.getElementsByClassName( 'loading' ); 
	images_spinner = document.getElementById( 'images_spinner' );
	image_count = document.getElementsByClassName( 'short' ); 		// No longer updates as images are removed. Just add another class. 
		// Tracking "short" instead counts the test images, leading to brief overcounting. But not every site uses thumbnails. 
	image_counter = document.getElementById( 'image_counter' ); 		// Two hard problems. 

	// Flickering on/off multiple times per second: bad.
	// Waiting an entire second between image loads: also bad. 
	// So... add another low-impact interval. Efficient? Not really. But eeeasy. 
	var spinner_interval = 	setInterval( function() { 
			if( submissions_loading.length == 0 ) { 		/* If we're done testing for new images in submissions */
				submissions_spinner.className = 'spacer'; 
			} else {  		/* If e.g. we reload a submission and it takes a while */
				submissions_spinner.className = 'submissions_loader'; 
			}

			if( images_loading.length == 0 ) { 		/* If we're done testing for new images in submissions */
				images_spinner.className = 'other_spacer'; 
			} else {  		/* If e.g. we reload a submission and it takes a while */
				images_spinner.className = 'images_loader'; 
			}

			image_counter.innerText = '' + image_count.length + ' images'; 
			/* Should arguably exclude loading images.  */
			/* Could be made accurate by counting calls to next_image. Right? Or incrementing some var when that succeeds. */ 
			/* Just count className="thumb". Right? Haha, I'm already double-counting because loaded thumbnails don't really go away. Yeah, count "thumb." */
	}, 1000 ); 

}