Eza's Gallery Swallower

Turn a page of thumbnails into high-res images

As of 2021-02-16. See the latest version.

// ==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?*
// @exclude    *#dnr
// @noframes
// @version     2.0
// @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. 

// Image Glutton: "Greasy Fork allows scripts with adult content, but they must be marked as such so that users can choose whether they see it. Your script has been marked as having adult content due to being for booru.org, bronibooru.com, derpibooru.org, donmai.us, e-hentai.org, e621.net, gelbooru.com, hypnohub.net, luscious.net, neko-sentai.com, nijie.info, paheal.net, rule34.xxx, rule34hentai.net, sankakucomplex.com, uberbooru.com, and youhate.us."
	// Assholes. 
	// So I can't release this with Gelbooru support, without banishing it to SleazyFork. 
	// Do I want to bother managing two versions? Doing the same damn thing, but some of it on -some- websites? Ugh. 
	// Maybe fork this v2.0 rejigger and rename v1.0 to Eza's Pixiv Somethingorother. Like InkBunny-only proto-Image Glutton. 
	// @description Display an entire gallery page as high-res images 
	// @description Turn a page of thumbnails into high-res images

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

// HentaiFoundry?
	// "<a class="thumbLink" href="/pictures/user/AmaZima/587206/Meet-Lottie-red-head-elf-girl"><span title="Meet Lottie, red head elf girl" class="fake_thumb" style="background-image: url(//thumbs.hentai-foundry.com/thumb.php?pid=587206&amp;size=350)"></span></a>"
	// //thumbs.hentai-foundry.com/thumb.php?pid=587206&amp;size=350
	// http://pictures.hentai-foundry.com/a/AmaZima/587206/AmaZima-587206-Meet_Lottie_red_head_elf_girl.png
	// ... but it'll accept http://pictures.hentai-foundry.com/a/AmaZima/587206/AmaZima-587206.png. We're in. 
	// document.querySelectorAll( 'a.thumbLink' ) > style="background-image: url(//thumbs.hentai-foundry.com/thumb.php?pid=587206&amp;size=350)"

// 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. 



	// 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 assumes the first image works. Maybe do cleanup in the interval?





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





// Replacement page. Not used immediately; it just makes more sense up here. 
var html = '';
// I'm only mildly sorry for all these stylesheets being separate. 

// Image style(s):
html += '<style> img.short { max-width: 90vw; max-height: 60vh; z-index: 10; } </style>'; 		/* I liked things short. */
//html += '<style> a.link[target="_blank"] > img.short { display: revert !important; } </style>';  // Aggravating fight against Gelbooru's "display:none". 

// 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>';  
// Dead spinners:
html += '<style> .spacer { position: absolute; width: 0px; height: 0px; } </style>'; 
html += '<style> .other_spacer { position: absolute; width: 0px; height: 0px; } </style>'; 	/* Arbitrarily smaller. */
html += '<style> #image_counter{ position: absolute; left: 70px; top: 5px; font-size: 33px; }  </style>'
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.' */

// Controls: 
html += '<style> .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; } .remover:hover { background-color:#bd2a2a; } </style>'; 
//html += '<style> .group { position: relative; } </style>'; 
html += '<style> .floating { z-index: 1; position: absolute; top: 50%; -ms-transform: translateY(-50%); transform: translateY(-50%); width: 120px; height: 120px; font-size:45px; } </style>'; 
html += '<style> .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; } .reloader:hover { background-color:#2abd2a; } </style>';

// Hidden thumbnails, probing to check for next image in multi-image sets:
html += '<style> .test { display:none } .thumb { display:none } </style>'; 

// Page elements:
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 images = document.getElementsByTagName( 'img' ); 		// Only used by Pixiv delay function
var links = document.getElementsByTagName( 'a' ); 

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 
var top_to_bottom = false; 		// Chronological order is confusing to describe. 
var single_image = false; 			// Pixiv-style multiple-image mangas, or Gelbooru-style single-image pages? 
var gather_items;  		// Per-site function to scrape page contents. 
var items;  					// Scraped contents of page. 
var page_search = '?p='; 		// URL break for page indicator. Pixiv default, why not. 
var page_step = 1; 				// page 2, page 3, page 4... versus image 40, image 80, image 120...
var formats = [ ".png", ".jpg", ".gif" ]; 		// File extensions. E.g. some sites do JPEG. 

// Per-site setup and gather functions:
switch( document.domain.replace( 'www.', '' ) ) {

	case 'pixiv.net':
		// Ever-useful test profile: https://www.pixiv.net/en/users/53625793 
		button_delay_function = () => 
			Array.from( images ).find( e => e.src.match( '250x' ) ) || 
			Array.from( links ).find( e => e.innerHTML.match( '240x' ) );

		gather_items = function() { 
			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' ) );
			things = things.concat( Array.from( links ) 		// Sort and reverse: side-effectful. Concat: functional. Go fuck yourself, Javascript. 
				.filter( a => a.innerHTML.match( '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. 
				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 = item.thumb
					.replace( '.png', '' )
					.replace( '.jpg', '' )
					.replace( '.gif', '' )
					.replace( '_p0', '_p' );   		// Remember to add a page number. 
				item.thumb_followup = item.thumb.split( '_p' )[1]; 
				item.image_followup = ''; 
				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. 
				console.log( item.image ); 
				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. 
		page_search = '&pid='; 
		page_step = 42 - 1; 		// Could be automatically detected before erasing page. 
		single_image = true; 
		formats.push( ".jpeg" ); 

		gather_items = function() { 
			let things = Array.from( links )
				.filter( a => a.href )
				.filter( a => a.href.indexOf( 's=view' ) > 0 );
			let items = new Array; 
			things.forEach( (v,i,a) => { 
				let item = new Object;
				item.page = v.href; 
				// https://gelbooru.com/index.php?page=post&s=view&id=4179699&tags=pink_background
				item.title = item.page.split( '&id=' )[1].split( '&' )[0]; 
				// https://img3.gelbooru.com/thumbnails/f5/c7/thumbnail_f5c7826072943fd72076ba9121b473f0.jpg
				// https://img3.gelbooru.com/images/f5/c7/f5c7826072943fd72076ba9121b473f0.jpg
				item.thumb = v.querySelector( 'img' ).src
					.replace( '.png', '' )
					.replace( '.jpg', '' )
					.replace( '.gif', '' )
				item.image = item.thumb
					.replace( '/thumbnails', '/images' )
					.replace( 'thumbnail_', '' ); 
				item.thumb_followup = ''; 
				item.image_followup = ''; 
				console.log( item ); 		// Debug 
				items.push( item ); 
			} ); 
			return items; 
		}
	break;

	// Note: e621 does -not- exclude blacklist, yet. 
	// Janky e621 issues seem to be because JS isn't allowed to run on the page? Somehow? It's not ScriptBlock. 
	// Erase head for now, but consider: e621's background looks sick here. (Did not work.)
	// Yeesh. Content Security Policy warnings in the console weren't for e621 scripts breaking - they say inline code is verboten. 
		// "The page’s settings blocked the loading of a resource at self." 
		// So... I'm running code on the page. I have completely eliminated all page content. 
		// Surely I can change that setting to allow "resource at self." Right, Javascript? 
	// https://stackoverflow.com/questions/37298608/content-security-policy-the-pages-settings-blocked-the-loading-of-a-resource
	// <meta http-equiv="Content-Security-Policy" content="default-src *; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' 'unsafe-eval' http://www.google.com">
	// Hmm. Might be intractable. Might also be Firefox-only, which is some bullshit. 
	// At the very least, remove the spinner, since it'll never go away otherwise. 
	// Aaaargh there's probably some horrible version of this where I fake on_error and on_load inside this script's interval function. 
	// Pages don't work yet. (I.e. prev/next page.) Search terms aren't preserved. 
	case 'e621.net':
		// https://e621.net/posts?tags=somik+mirror
		trigger_size = [ 15, 50, 16, 5 ]; 		// Left, top, font-size, padding. 
		page_search = '?page='; 		// Needs filter for &tags=etc. Hell, just define page_number here, as a global variable. 
		page_step = 1;
		single_image = true; 
/*
		html = '<meta http-equiv="Content-Security-Policy" content="default-src *; ' 		// Nope. 
		html += "style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' 'unsafe-eval' http://www.google.com"
		html += '">'; 

// 		html = '<meta http-equiv="Content-Security-Policy" content="default-src *; ' + "style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' 'unsafe-eval' http://www.google.com" + '">' + html; 	 	// Still nope.
*/	
		gather_items = function() { 
			let things = Array.from( links )
				.filter( a => a.href )
				.filter( a => a.href.indexOf( '/posts/' ) > 0 )
				.filter( a => a.querySelector( 'img' ) )
			let items = new Array; 
			things.forEach( (v,i,a) => { 

				let item = new Object;

				// https://e621.net/posts/1333873?q=somik+mirror
				item.page = v.href; 
				item.title = item.page.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
				item.thumb = v.querySelector( 'img' ).src
					.replace( '.png', '' )
					.replace( '.jpg', '' )
					.replace( '.gif', '' )
				item.image = item.thumb
					.replace( '/preview', '' );
				item.thumb_followup = ''; 
				item.image_followup = ''; 

				items.push( item ); 

			} ); 
			return items; 
		}
	break;

}

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( button_delay_function ) { 		// Note: checking for presence of function. (Could use typeOf.) 
	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 ); 
}

// 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. 

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

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

	// Previous page / Next page links
	// Should probably be absorbed into per-site functions. Boorus are weird. 
	let links = document.getElementById( 'links' ); 		// Has to go after document = html, duh.
	let page_number = 1;
	if( window.location.href.indexOf( page_search ) > 0 ) { 		// Asinine that String.match chokes on ?. 
		page_number = parseInt( window.location.href.split( page_search )[1] ); 	// https://www.pixiv.net/en/users/12345/artworks?p=2 
	} 
	let link_html = ' <a href="' + window.location.origin + window.location.pathname
			+ page_search + ( page_number + page_step ) + '">Next page</a>'; 
	if( page_number > 1 ) { 
		link_html = '<a href="' + window.location.origin + window.location.pathname 
			+ page_search + ( page_number - page_step ) + '">Previous page</a> -' 
			+ link_html; 
	} 
	links.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 submission_remover = document.createElement( 'button' );  		// Erase whole submission. 
		submission_remover.innerText = '❌'; 
		submission_remover.setAttribute( 'onclick', 'document.getElementById( "' + item_key + '" ).innerHTML = "";' );
		submission_remover.className = 'remover'; 		// Add some CSS to float left or whatever. 

		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 reloader = document.createElement( 'button' ); 
		reloader.innerText = '⟳'; 
		reloader.setAttribute( 'onclick', 'let e=document.getElementById( "' + item_key 
			+ '" ); e.innerHTML = ""; e.dataset.page_number=0; e.className="ready"' );
		reloader.className = 'reloader'; 

//		let span = document.createElement( 'span' );  		// This comes and goes as I wrestle with CSS. I think I'm done with it. I think. 
//		span.id = item_key + '_' + '0'; 

		let bottom_submission_remover = submission_remover.cloneNode(); 		// Why doesn't this include innerText? 
		bottom_submission_remover.innerText = '❌';  	
		bottom_submission_remover.setAttribute( 'onclick', 'document.getElementById( "' + item_key 
			+ '" ).innerHTML = ""; document.getElementById( "' + item_key + 'container' + '" ).scrollIntoView()' ); 		
		// 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. 

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

		centered.appendChild( container ); 

		basket.dataset.page_number = 0; 
		if( single_image ) { 
			basket.dataset.page_number = -1; 	// Why signal in-bounds? Just check single_image inside the ready/next interval. 
		} 
		basket.classList.add( "ready" ) 
	}  



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



	// Load more images in "ready" submissions, when available. 
	var ready_elements = document.getElementsByClassName( 'ready' ); 
	var interval_object = setInterval( function() { 
			// Single-image mode: add a span to the ready basket, containing the single image (in all tried formats) and its controls (an X to remove the span). 
			// Multi-image mode: do that, but also add try-format thumbnails for the next page, with onLoad code to re-ready the basket. 
			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 ); 
				ready_element.dataset.page_number = 1 + manga_page; 
				if( single_image ) { manga_page = ''; } 		// Mild kludge. 

				// 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 = items[ item_id ].image + manga_page + items[ item_id ].image_followup + format;
					console.log( items[ item_id ].image + manga_page + items[ item_id ].image_followup + format ); 
					png.setAttribute( 'onerror', 'this.parentElement.remove();' ); 		// Remove apng / ajpg / agif link as well. 
					png.setAttribute( 'onload', 'this.classList.remove( "loading" );' ); 
					// "if no_thumbnails" would make the next line useful. Probe with real images. Same results, but slower. 
//					png.setAttribute( 'onload', 'this.classList.remove( "loading" ); this.parentElement.parentElement.parentElement.classList.add( "ready" );' ); 
					// This eliminates the thumb-try stuff. No thumbnails required. It just means waiting for the big images to load. 
					// We could get away with "on resolve" or somesuch - i.e., if this doesn't 404 or 403, trigger some code. 

					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.setAttribute( 'onclick', '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' ) ); 

				if( single_image ) { return; } 



				// Thumb-try stuff:

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

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

		}, 200 ); 



	// ----- // 		🍭 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. 
	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. 
	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 ); 

}