Sleazy Fork is available in English.

Eza's Gallery Swallower

Turn a page of thumbnails into high-res images

2021-03-07 يوللانغان نەشرى. ئەڭ يېڭى نەشرىنى كۆرۈش.

// ==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://safebooru.org/index.php?page*

// @include     https://e621.net/posts*
// @include     https://e926.net/posts*
// @include     https://e621.net/pools/*
// @include     https://e926.net/pools/*

// @include     https://*.booru.org/*s=list*

// @include     /https?\:\/\/www\.hentai-foundry\.com\//

// @include     https://baraag.net/@*
// @include     https://botsin.space/@*
// @include     https://equestria.social/@*
// @include     https://mastodon.art/@*
// @include     https://mastodon.social/@*

// @include     /https?\:\/\/rule34\.paheal\.net\//

// @include     https://derpibooru.org/*

// @include     https://inkbunny.net/*
// @exclude    https://inkbunny.net/s/*

// @include     https://www.furaffinity.net/*

// @include     https://danbooru.donmai.us/*

// @include     https://rule34.xxx/*

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

// User options:
var default_size = "short"; 		// Choices: short, fit_width, fit_height, fit_window, full. 
var new_image_rate = 50; 		// Milliseconds between loading images. E.g. 10 for instant, 200 for gradual. 
var fetch_rate = 250; 				// Slower rate for fetching than for loading images, to avoid rate-limiting. 
var top_to_bottom = false; 		// Default order. True: as seen on the page. False: reversed, which is usually chronological. 
var keyboard_controls = true; 		// (We don't actually have keyboard controls yet.) 



// 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. 
// I could trivially turn this into a Pixiv Fixiv replacement - right? All I need is any image URL. No JSON shenanigans. 

// Change the BG color to something dark. (Ego says #324.) 
// 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. 

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

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

// 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. 
// 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. 
// Ooh, Array.find(). Pass it a test function and it'll return the first element that matches. 

// Oh, file-extension issues in Universal Scraper don't apply here. 

// 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 
// 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.) I guess link it to the page. Big _new target. 

// Other Mastodon instances? Pawoo. 
/*
https://mastodon.social/tags/art
Array.from( new Set( Array.from( document.querySelectorAll( '.display-name__account' ) ).map( v => v.innerText.split('@')[2] ) ) ).join( '\n' ) 
mastodon.art
mastodon.online
mastodon.social
pawoo.net
queer.af
gensokyo.social
infosec.exchange ?

slashine.onl
mastodont.cat
social.adamasnemesis.com
chitter.xyz
qoto.org
mastodon.sdf.org
photog.social
1929.com
equestria.social
fosstodon.org
mamot.fr
mastodon.technology
mastodon.cloud
wandering.shop
vis.social
todon.nl
pixelfed.social
libranet.de
social.tchncs.de
shrike.club
layer8.space
alive.bar
mastodon.ml
artalley.social
mstdn.social
mastodon.xyz
coletivos.org
framapiaf.org
chaos.social
mograph.social
octodon.social
embassy.social
mast.eu.org
plush.city
edolas.world
paypig.org
toot.elyat.im
friends.deko.cloud
www.librepunk.club
distrotoot.com
toot.wales
redroo.ml
artoot.xyz
Half of these seem to be crap. And it's still just a tiny sample. Yeesh. 
*/
// I am alarmingly close to forking this for Mastodon, doing @include *, and testing for e.g. h-cite / h-entry. 
	// I don't want to @include * in general - this is not a general-purpose script. 
	// Even a fetch-based version would only be as flexible as Image Glutton. 
	// ... I've accidentally made this @include * friendly, by exclusively using button_delay_function. 
		// If it defaulted to false I could use this anywhere, and only specific switch-case / match domains would get better values. 
		// Or I could skip the interval entirely if button_delay_function remains at a default null. 
			// (Probably better to do so if gather_items remains at a default null.) 

// Ayrion.com / Eka's Domain? 
	// https://static.aryion.com/g4/thumb/662671-65284-1murk5-thumb.auto.jpg
	// https://aryion.com/g4/data/662671-65284-1murh3.jpg/Slimshod-662671-loli_kitchen_girl_version_2020_01_18_1.jpg
	// https://aryion.com/g4/data/662671-65284-1murh3.jpg/Slimshod-662671.jpg works. 1murk5 doesn't. 
	// But then there's shit like:
	// https://static.aryion.com/g4/thumb/647381-65284-thumb.2640-330-330-330.crop.jpg
	// https://aryion.com/g4/data/647381-65284-1bx9vuq.png/Slimshod-647381-cocktoberfest.png
	// https://aryion.com/g4/view/647381 so the link is no help. 
	// <li class="gallery-item" id="647381"><div><a class="thumb" href="/g4/view/647381"><img src="//static.aryion.com/g4/thumb/647381-65284-thumb.2640-330-330-330.crop.jpg" alt=""></a><p class="item-title">Cocktober Fest [cooking] by MisticHobo</p><p class="g-small"><span>Views: 2,501<span class="biicon11 type-Images"></span></span></p></div></li>
	// Nope, needs fetching. 
	// Sometimes I worry about the mental health of people reading this for the code. Sorry, folks. I'm not here for boring shit. 

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

// 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. 
	// Middle-click behavior (since they're not links) wouldn't interfere, but wouldn't be discoverable. 

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

// Spent a while agonizing over performance vis-a-vis live HTMLCollections vs. querySelector, and found out it super doesn't matter. 
	// Setting up getElements and then only checking the first one has approximately zero impact on a modern PC. 
	// And the impact of setInterval seems to be approximately dick-all, so long as the timeout isn't zero or effectively zero. 
	// The upshot is that I set the timer to 10 ms and pages just -fly.- The "swallow gallery" button works immediately. Images load instantly. 
		// The latter is a mild concern, because sites might notice. An ideal server interaction looks just like opening a bunch of pages.

// Might move image count to the controls area. 
	// Its text already implies "image size" and "image order." But mostly I'm bugged that it doesn't line up with other text.

// Scolling controls: stick them against the left edge, level with top of each image, at a higher Z than the image. 
	// The goal is that they 'stay in place' when you click them. I.e. the next page's backwards / forwards buttons take the same position. 
	// Whole-submission backwards / forwards buttons go to the left and right of the submission title / link / remover. 
		// (Putting them on either side keeps the title centered.) 
		// Since these goofy circles are Like That for touchscreen-style separation, also include spacers. 
		// Previous submission, spacer, reload submission, title, remove submission, spacer, next submission. 

// Fetch mode should introduce a new status besides "ready". 
	// E.g. set a div to "fetch" so it'll grab the page and replace the items[n] page URL with an image URL. 
	// Nah. Keep using "ready," but allow different actions. 
	// If there's item.page_url, fetch that and return, with a .then set up to parse the HTML, set the image URL, and re-ready. 
		// Or, slight variation: .then store the Promise object and parse / display in the 'when ready' interval. 
	// Multi-page sites get screwy. No surprise. InkBunny's always a problem. 

// Alright, I should probably support videos. 
	// https://baraag.net/@ThousandArms/105668327758530515#dnr#&dnr 

// Thematically, the image-size controls should probably be blue. Meh. They're distinct enough for not being grey. 

// Youhate.us? Down. 

// JabArchives.com? Comics deserve it. 

// FurAffinity maintains the infinite loop when you fuck with the page and change window size. God -dammit- FA. 
	// Okay. I can use ScriptBlock, and the effect doesn't happen. So it's some stupid Javascript, not, like, a horrifying CSS loop. 
	// Getting rid of document.head has no effect. So it's something in document.body. 
	// Array.from( document.querySelectorAll( 'img' ) ).forEach( v => v.remove() ) - no impact. 
	// a, no impact. div, no impact, despite there being nothing left. Start over. 
	// div from fresh start: hangs. a from fresh start: no impact. div after a: still no impact. Uh -huh.- 
	// form: no impact. section: hangs. 
	// Removing <section id="gallery-gallery" class="gallery no-padding align…er no-artistname s-200 "> hangs. 
	// That mostly contains the thumbnails as "figure" elements. Removing all figures: no impact. 
	// Replacing that section's innerHTML with itself: no impact. 
	// Replacing the whole page's innerHTML: with itself, after that: hangs. 
	// Instead removing that section after replacing its innerHTML with itself: hangs. 
	// Alright, what about children? 
	// document.querySelector( '.section-body').remove() - hangs. 
	// .submission-list - hangs. 
	// .aligncenter - no effect. 
	// document.querySelector( 'section .gallery').remove() - hangs. 
	// Right below that is a script that calls  _fajs.push( [ 'init_gallery', 'gallery-gallery' ] ). So what if I change the ID of gallery-gallery? No impact. 
	// And yet - document.querySelector( '#gallery-gallery').innerHTML = ''; has no impact. 
	// Ditto replacing its innerHTML with itself. 
	// document.querySelector( 'section .submission-list') - hangs. (Contains #gallery-gallery, the script, etc.) 
	// document.querySelector( 'section script').remove() - no impact. 
// And yet... there are several things I've done where the hang is prevented. That's all I really want. 
	// I don't have to fully understand this to exploit it. I want it to do nothing. 
	// E.g. I can do Array.from( document.querySelectorAll( 'a' ) ).forEach( v => v.remove() ), then document.body.innerHTML = '' - no impact. 
// Removing all 'a' elements works here, but is obviously goofy. It wouldn't fit Image Notch / Universal Scraper. 
	// document.querySelectorAll( '#gallery-gallery' )[0].classList = [] throws a lot of "Uncaught TypeError" but does not hang... on its own. 
// https://stackoverflow.com/questions/19469881/remove-all-event-listeners-of-specific-type
	// window.addEventListener( 'resize', function( event ) { event.stopPropagation(); }, true ) - nope. 
// Oh, Firefox proper threw an error:
	/*
	Script terminated by timeout at:
	_reflow_gallery@https://www.furaffinity.net/themes/beta/js/script.js?u=2021022800:885:275
	createResponder/<@https://www.furaffinity.net/themes/beta/js/script.js?u=2021022800:781:181
	EventListener.handleEvent*observeStandardEvent@https://www.furaffinity.net/themes/beta/js/script.js?u=2021022800:753:144
	observe@https://www.furaffinity.net/themes/beta/js/script.js?u=2021022800:752:21
	init_gallery@https://www.furaffinity.net/themes/beta/js/script.js?u=2021022800:877:90
	push@https://www.furaffinity.net/themes/beta/js/script.js?u=2021022800:914:121
	@https://www.furaffinity.net/themes/beta/js/script.js?u=2021022800:914:184
	*/
	// But that is not super helpful. The line numbers don't line up with that script file. 
	// _reflow_gallery is defined twice as far down. Picking through it. querySelectorAll (jQuery style) for images. But it--
		// Wait. _reflow_gallery() is in the main scope. I can just overwrite it. 
		// THAT SOMEHOW DOESN'T WORK. I'm not angry, but holy shit, am I surprised. Really thought that was a slam-dunk. 
		// init_gallery, also no. createRespondor, observe, and push are not functions (or are not in-scope). 
		// _reflow_gallery does contain some while loops - while( image.length > 0 ), while( row_width < container_width ). 
		// Meh. It works here. 

// 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. 
	// Just happened to a bare image on i.reddit.com domain. Pretty sure it's a Pale Moon issue. 

// fetch( 'https://www.pixiv.net/artworks/88268868' ).then( response => response.text() ).then( text => { doc = document.createElement( 'html' ); doc.innerHTML = text; console.log( text ) } )



	// To do:

// Scrolling controls. Previous / next per-image and per-submission. 
	// Eventually turn these into keyboard commands, presumably with some interval to update which image they're relative to. 
	// Submissions already have IDs what we scrollIntoView via thumbnails. 
	// Individual images get dicey because we can remove them. I'd rather not manage a linked list separate from the DOM. 
		// ... I guess the DOM itself works? There's gotta be some nextChild thing that gets the job done. 
		// Can I generalize to some this.nextElement( '.class' ).scrollIntoView? 
		// nextElementSibling doesn't really do it, barring a ton of finagling with hierarchy. 
		// This is not computationally complicated. It's linear. I could just querySelectorAll and .find( this ). 
		// Array.from( document.querySelectorAll( 'a' ) ).find( e => e = x ), where x is an <a>, returns... x. Thanks. Gimme the index. 
		// findIndex always returns 0. I guess I could test for e.dataset.whatever > this.dataset.whatever? And invert both when reversed. 
		// Durrrr == not =. 
	// Basically just unfucking the CSS at this point. 
	// Putting Next above Previous is probably a coin-flip I'll regret. Change it immediately, if ever, because right now it'll affect about one person. 

// Put alt/title (onhover) text on size / order buttons? 

// Kind of inconvenient having the previous / next and removal controls on opposite sides. Hmm. 



	// 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. 
	// Initialize to static, I guess. 

// Reversing order doesn't reverse the previous / next submission buttons. 

// images_spinner doesn't work on FurAffinity and I have no idea why. 
	// The spinners don't line up on Danbooru and it's less a mystery than an annoyance. 



// Changes since last version:
	// Fixed navigation buttons with reversed order. (Original approach was numbered, so "next" was +/- 1. Now it's by DOM order. Next is always +1.)
	// Immediately swapped order of per-image navigation buttons. My apologies to both users who've already developed muscle memory. 





// ------------------------------------ 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[ ".short img" ] = "max-width: 90vw; max-height: 60vh; z-index: 10; vertical-align: middle;"; 		// Class is for <body>.  
style_rules[ ".full img" ] = "max-width: initial; max-height: initial; z-index: 10; vertical-align: middle;"; 			// "initial" for fake_css(). 
style_rules[ ".fit_width img" ] = "max-width: 90vw; max-height: initial; z-index: 10; vertical-align: middle;";  	// Leaving space for big X.
style_rules[ ".fit_height img" ] = "max-width: initial; max-height: 95vh; z-index: 10; vertical-align: middle;";  
style_rules[ ".fit_window img" ] = "max-width: 90vw; max-height: 95vh; 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;';

// Button DRY:
style_rules[ '.eza_button' ] = 'border-radius: 50%; width: 60px; height: 60px; text-align: center; display: inline-block; cursor:pointer; line-height: 20px; font-size:33px !important; padding: 10px 10px; text-decoration: none; font-family: sans-serif !important;';

// Remove / reload controls: 
style_rules[ '.remover' ] = 'color:#dd2e44 !important; background-color:#d7dbd8; border:1px solid #ab1919;';
style_rules[ '.remover:hover' ] = 'background-color:#992a2a;';
style_rules[ '.floating' ] = 'z-index: 1; position: absolute; top: 50%; -ms-transform: translateY(-50%); transform: translateY(-50%); width: 120px; height: 120px; font-size:72px !important;';
style_rules[ '.reloader' ] = 'color:#194d19 !important; background-color:#dbd7d8; border:1px solid #19ab19;';
style_rules[ '.reloader:hover' ] = 'background-color:#2abd2a;';

// Previous / next buttons, per-submission:
style_rules[ '.navigation_button' ] = 'color:#123 !important; background-color:#d7dbd8; border:1px solid #234;';
style_rules[ '.navigation_button:hover' ] = 'background-color:#14f;';
style_rules[ '.navigation_float' ] = 'z-index: 11; position: absolute; left: 0px; top: initial;';  
style_rules[ '.navigation_next_image' ] = 'top: 72px;';  

style_rules[ '.button_spacer' ] = 'visibility: hidden; width: 30px; height: 30px;'; 

// Image-size controls:
style_rules[ '#controls' ] = 'float: right; font-size: 33px;';
style_rules[ '.control_button' ] = 'color:#FFF; background-color:#363;'; 
style_rules[ '.control_button:hover' ] = 'background-color:#282;'; 

// Previous / next links, at the bottom:
style_rules[ '.page_links' ] = 'font-size: 33px;';

// Thumbnail container and fixed-size thumbnails:
style_rules[ '.thumbnails_container' ] = 'display: grid; grid-gap: 5px;';  		// Spans can't have fixed size, divs can't flow sensibly. Argh. 
style_rules[ '.thumb_box' ] = 'width:100px; height:100px; display: inline-block; overflow: hidden;';
style_rules[ '.thumbnail_image' ] = 'width:100px;';

style_rules[ 'body' ] = 'line-height: 1.425 !important'; 		// Low values let thumbnails overlap image controls. Gelbooru picks 1.42857143... for some reason. 

// 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:
// Either I bodged the implementation of style_rules to a <style> block, or there's some other obstacle to animations. So these are separate. 
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>';  




	// ----- //			Page elements



// Floaty stuff:
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. */

// Structure:
html += '<br><span id="controls" class=""></span><br><br><br><br>';
html += '<span id="thumbnails_container"></span><br><br>';
html += '<br><br><center><span id="centered" class="' + default_size + '"></span></center>';  		/* Where most 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 = ( () => true ); 	// Don't show Swallow Gallery button while this returns false. (Alternative to @includes.) 
var automatic_pagination = false; 		// Pixiv-style p0, p1, p2, etc. 
var gather_items;  		// Per-site function to scrape current page contents. 
var image_from_dom; 	// Per-site function to scrape fetched HTML (if relevant). 
var items;  					// Scraped contents of page. 
var formats = [ ".png", ".jpg", ".gif", ".jpeg" ]; 		// File extensions for guessing URLs. 
var page_number = 1; 				// Default.
var next_page, previous_page;  	// URLs of obvious purpose. 
var probe_followup = '';  		// Used to be part of "items," per-item, but only Pixiv uses them, and they're standard. 
var image_followup = ''; 		// Ditto. 
var force_style = false; 			// If sites don't allow inline CSS, apply style_rules to each element. 
var centered, controls, thumbnails_container; 		// Spans that are already accessed globably, but were local "let" variables? Eh. 
//var nav_direction = 1; 		// Stopgap. 1: "next" button increments container ID. -1: decrements. It's for reversed order. 




	// ----- //			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; 
}

// Remove .jpg, .png, etc. from thumbnails, because full-size image formats don't always match. 
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; 
} 

// Force elements to have inline style, when in-page CSS is prevented by CSP. I hate the modern web. 
function fake_css( parent ) { 
	if( force_style ) { 		// Not ideal practice? But it beats repeating "if( force_style ) { fake_css(); }" when that's the only time it'd happen. 
									// On reflection, it's more clear to have 'if condition then call function' the two or three times it's relevant. Refactor. 
									// Better refactor: rename this consider_faking_css(). 
		if( parent == null ) { parent = document; } 		// Apply everywhere by default
		for( selector in style_rules ) { 		// Global
			Array.from( parent.querySelectorAll( selector ) )
				.forEach( element => { 
					element.style = element.style.cssText + style_rules[ selector ]; 
				} )
		} 
	}
} 

// Fetch HTML, interpret as DOM, pass DOM to standard get-image(s) function. 
function standard_fetch( item_object, span ) { 
	fetch( item_object.page )
		.then( response => { 		// On fetch error, retry: set "ready" in 10-20 seconds. 
			if( ! response.ok ) { setTimeout( () => span.classList.add( "ready" ), 1000 * ( 10 + Math.random() * 10 ) ); } 
			return response; 
		} )
		.then( response => response.text() ) 			// Aggravating boilerplate.
		.then( text => { 
			doc = document.createElement( 'html' );
			doc.innerHTML = text; 
			item_object.image = image_from_dom( doc ); 
			span.classList.add( "ready" ); 
		} )
} 

// From an origin element inside a selector, scroll to the previous / next instance of that selector. 
// Really should be standard as .nextQuery() even if CSS can't select up. It's JS -using- CSS. You allowed .closest! 
function scroll_to_next( origin, selector, direction ) {
	let reference = origin.closest( selector ); 		// Ancestor or self, whatever. Pass in "this" and don't worry. 
	let list = Array.from( document.querySelectorAll( selector ) ); 
	let index = list.findIndex( e => e == reference ); 
	list[ index + direction ].scrollIntoView(); 
}



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



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

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

	case 'pixiv.net':
		probe_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 
		// Series: https://www.pixiv.net/user/2258616/series/38203, https://www.pixiv.net/user/55117629/series/87453 
		button_delay_function = () => 
			document.querySelector( 'img[src*="250x"]' ) || 		// /users
			document.querySelector( 'img[src*="360x"]' ) || 		// /series
			document.querySelector( 'a *[style*="c/240"]' ); 		// bookmark_new_illust

 		// https://www.pixiv.net/user/55117629/series/87453?p=2
		// Pixiv's fake links mess this up sometimes. Nothing I can do - the browser is confused about the URL. 
		if( args["p"] ) { page_number = parseInt( args["p"] ); } 
		next_page = window.location.origin + window.location.pathname + "?p=" + (page_number + 1)
		if( page_number > 1 ) { previous_page = window.location.origin + window.location.pathname + "?p=" + (page_number - 1); }

		gather_items = function() { 
			// https://i.pximg.net/c/250x250_80_a2/img-master/img/2020/05/15/04/50/43/81571620_p0_square1200.jpg 
			let thumbs = Array.from( document.querySelectorAll( 'a[href*="/art"] img[src*="c/250"]' )  ) 	// en/users/12345 
				.map( img => img.src )
			// https://i.pximg.net/c/360x360_70/img-master/img/2020/06/20/11/14/47/82439841_p0_square1200.jpg
			if( window.location.href.match( 'series' ) ) { 
				thumbs = Array.from( document.querySelectorAll( 'a[href*="/art"] img[src*="c/360"]' ) ) 		// /series
					.map( img => img.src ) } 
			// url("https://i.pximg.net/c/240x240/img-master/img/2021/02/17/04/05/41/87838098_p0_master1200.jpg") 
			if( window.location.href.match( '_new' ) ) { 
				thumbs = Array.from( document.querySelectorAll( 'a *[style*="c/240"]' ) ) 								// bookmark_new_illust  
					.map( div => div.style.backgroundImage
						.match( /".*"/ )[0].slice( 1, -1 ) ) } 		// Remove url("/") from either end. 

			return thumbs.map( t => { 
				// Tried doing this neatly with regexes, but some Ugoira thumbnails  88267833_master1200, instead of 88267833_p0_square1200. 
				let image_part = t.split( '/img/' )[1].split( '_' )[0] + '_p'; 		// 2021/03/07/07/11/52/88269944_p
				let submission = t.split( '/' ).pop().split( '_' )[0]; 		// 88269944
				return new Object( { 
					page: 		'https://www.pixiv.net/artworks/' + submission,
					title: 			submission,
					thumb: 	t,
					probe: 		'https://i.pximg.net/c/48x48/img-master/img/' + image_part,
					image: 		'https://i.pximg.net/img-original/img/' + image_part
				} ) 
			} )
		}
	break; 

	// https://gelbooru.com/index.php?page=post&s=view&id=2135704 - bad submission crashes gather_items. 
	// https://gelbooru.com/index.php?page=post&s=list&tags=saberfish+aftersex+hug 
	// Huh. Only causes problems on the Comments page, not post list. 
	case 'gelbooru.com':
	case 'safebooru.org': 
		// https://gelbooru.com/index.php?page=post&s=list&tags=shuujin_academy_uniform+chair+1girl+pink_background 
		trigger_size = [ 15, 50, 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; 
		pages_at_once = 42; 
		if( window.location.host == 'safebooru.org' ) { pages_at_once = 40; } 
		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. 
		if( page_number > 1 ) { 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
					// https://safebooru.org/thumbnails/3259/thumbnail_91588bf615a2d239d5b09c7c959236bc17b58ca6.jpg?3389404
					// https://safebooru.org//images/3259/91588bf615a2d239d5b09c7c959236bc17b58ca6.jpg - Image Glutton
					// https://safebooru.org//images/3259/91588bf615a2d239d5b09c7c959236bc17b58ca6.jpg?3389404 - from page
					thumb: 	v.querySelector( 'img' ).src,
					image: 		scrub_extensions( v.querySelector( 'img' ).src )
						.replace( '/thumbnails', '/images' )
						.replace( 'thumbnail_', '' ) 
						.replace( '_thumbnail', '' ) 		// Safebooru
						.split( '?' )[0] 						// Safebooru 
				} ) )
		}
	break;

	case 'e621.net':
	case 'e926.net':
		// https://e621.net/posts?tags=somik+mirror
		trigger_size = [ 15, 50, 16, 5 ]; 		// Left, top, font-size, padding. 
		if( window.location.href.indexOf( '/pools/' ) > 0 ) { top_to_bottom = true; } 

		// 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. 
		if( page_number > 1 ) { 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( '?' )[0],
					// https://static1.e621.net/data/preview/37/75/3775cd8664c688f98a41780f6796ce86.jpg
					// https://static1.e621.net/data/37/75/3775cd8664c688f98a41780f6796ce86.png
					thumb: 	v.querySelector( 'img' ).src,
					image: 		scrub_extensions( v.querySelector( 'img' ).src )
						.replace( '/preview', '' )
				} ) )
		}
	break;

	// http://www.hentai-foundry.com/users/FaveUsersRecentPictures?username=Example
	// http://www.hentai-foundry.com/pictures/user/AmaZima
	// http://www.hentai-foundry.com/pictures/user/AmaZima/scraps
	// Not http://www.hentai-foundry.com/pictures/user/AmaZima/875960/Spirit-lantern
	// Not http://www.hentai-foundry.com/user/AmaZima/profile 
	// Haha, http://www.hentai-foundry.com/pictures/user/Underrock/883176/MTWD-Ch.-II-Pg.-16#dnr#&dnr shows both PNG & JPG. 
	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. 
		button_delay_function = () => 
			document.querySelector( 'a.thumbLink' )

		// http://www.hentai-foundry.com/pictures/user/AmaZima/page/9
		// http://www.hentai-foundry.com/users/FaveUsersRecentPictures?username=Ezalias&page=2
		let page_indicator = '/page/'; 
		if( window.location.href.indexOf( '/users/' ) > 0 ) { page_indicator = '&page='; } 		// Followed users' recent pictures
		if( window.location.href.indexOf( page_indicator ) > 0 ) {  
			page_number = parseInt( window.location.href.split( page_indicator ).pop() ); 
			if( page_number > 1 ) { previous_page = window.location.href.split( page_indicator )[0] + page_indicator + (page_number - 1); }
		} 
		next_page = window.location.href.split( page_indicator )[0] + page_indicator + (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: 	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.
	// Maybe delay when "ready" again for image-array sites, so we don't instantly re-ready this one submission. Avoid linearity. 
	// 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. 
	// https://baraag.net/@MahmaPuu/105784936076784125 - video that breaks gather_items. "TypeError: data is undefined."
		// Apparently a "Video" component instead of "MediaGallery". No .media property. Argh. 
	case 'baraag.net':
	case 'equestria.social':
	case 'botsin.space': 
	case 'mastodon.art':
	case 'mastodon.social':
		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[0] ) { 
			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
		} 

		gather_items = function() { 
			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

						if( data ) { 		// Clunky workaround for some video submissions. Only some. 
							item.thumb = data.map( v => v.preview_url )[0]; 	// One thumbnail per submission. 
							item.image = data.map( v => v.url ); 
						} 
						return item; 
				} )
		} 
	break; 

	// http://rule34.paheal.net/post/list/Marco_Diaz%20Polyle/1 
	case 'rule34.paheal.net':
		formats = [""];

		button_delay_function = () => document.querySelector( '.shm-thumb' ); 

		let parts = window.location.href.split( '/' );
		page_number = parseInt( parts.pop() );  	// Side-effectful pop(). 
		if( isNaN( page_number ) ) {  		// Basically only http://rule34.paheal.net/post/list 
			page_number = 1;
			parts = window.location.href.split( '/' );
		}
		next_page = parts.join( '/' ) + '/' + (page_number+1);
		if( page_number > 1 ) { previous_page = parts.join( '/' ) + '/' + (page_number-1); }

		gather_items = function() { 
			// https://peach.paheal.net/_thumbs/421e200e2cde9a3b428a6e0df94a6492/thumb.jpg
			// https://peach.paheal.net/_images/421e200e2cde9a3b428a6e0df94a6492/3993741%20-%20Marco_Diaz%20Polyle%20Star_vs_the_Forces_of_Evil.png
			// http://rule34.paheal.net/post/view/3993741#search=Marco_Diaz%20Polyle 
			// <a href="https://peach.paheal.net/_images/421e200e2cde9a3b428a6e0df94a6492/3993741%20-%20Marco_Diaz%20Polyle%20Star_vs_the_Forces_of_Evil.png">File Only</a>
			return Array.from( document.querySelectorAll( '.shm-thumb' ) ) 
				.map( v => new Object( { 
					page: 		v.querySelector( '.shm-thumb-link' ).href,
					title: 			v.querySelector( 'img' ).id.replace( 'thumb_', '' ), 
					thumb: 	v.querySelector( 'img' ).src,
					image: 	scrub_extensions( v.querySelector( 'a[href*="/_images"]' ).href )
				} ) )
		}
	break;

	// https://rule34.xxx/index.php?page=post&s=list&tags=davepetasprite%5e2+
	case 'rule34.xxx':
		trigger_size = [ 150, 5, 16, 5 ]; 

		button_delay_function = () => document.querySelector( '.thumb' ); 

		if( args.pid ) { page_number = parseInt( args.pid ); } else { page_number = 0; }
		pages_at_once = 42; 
		next_page = document.querySelector( '.pagination a[href*="pid='+(page_number+pages_at_once)+'"]' )
		next_page = next_page ? next_page.href : null; 
		previous_page = document.querySelector( '.pagination a[href*="pid='+(page_number-pages_at_once)+'"]' )
		previous_page = previous_page ? previous_page.href : null; 

		gather_items = function() { 
			// https://miami.rule34.xxx/thumbnails/3500/thumbnail_62b3adfaa4d100c4a6fc7d419f61dd49.jpg?3944961
			// https://us.rule34.xxx//images/3500/62b3adfaa4d100c4a6fc7d419f61dd49.png?3944961
			// Should probably generalize some .fakeExtension to search/replace, instead of assuming they go on the end. 
			return Array.from( document.querySelectorAll( '.thumb' ) ) 
				.map( v => new Object( { 
					page: 		v.querySelector( 'a' ).href,
					title: 			v.id,
					thumb: 	v.querySelector( 'img[src*="/thumbnails"' ).src,
					image: 	scrub_extensions( v.querySelector( 'img[src*="/thumbnails"' ).src )
						.replace( /\/\/.*\.rule/, '//us.rule' ) 		// E.g. //miami.rule34.xxx -> //us.rule34.xxx
						.replace( '/thumbnails', '/images' )
						.replace( 'thumbnail_', '' )
						.split( '?' )[0]		// Fingers crossed. 
				} ) )
		}
	break;

	// Note: this won't show images with "Suggestive" or otherwise not-the-image thumbnails. 
		// It obeys your blacklist, and if you're not signed in, it obeys the default blacklist.
		// This behavior does not match how Image Glutton redirects through those warnings.
		// I'm okay with it, here, because other sites in this script obey blacklists. 
		// Though I should probably .filter for known not-the-image thumbnails so nothing shows up. Hm. 
	case 'derpibooru.org':
		trigger_size = [ 15, 55, 16, 5 ]; 		// Left, top, font-size, padding. 
		force_style = true; 	

		button_delay_function = () => document.querySelector( 'a[title*="Tagged"]' ); 

		// https://derpibooru.org/tags/artist-colon-jargon%2Bscott?page=2&q=artist%3Ajargon+scott
		// https://derpibooru.org/images?page=2
		// https://derpibooru.org/tags/artist-colon-jargon%2Bscott?page=2&q=artist%3Ajargon+scott?page=3 does not work. Arse. 
		page_number = parseInt( args.page ); 
		next_page = document.querySelector( 'a[href*="page=' + (page_number + 1)  + '"]' ); 
		if( next_page ) { next_page = next_page.href; }
		previous_page = document.querySelector( 'a[href*="page=' + (page_number - 1)  + '"]' );
		if( previous_page ) { previous_page = previous_page.href; } 		// I despise that this is the necessary pattern. Let null.whatever safely return null. 

		gather_items = function() { 
			return Array.from( document.querySelectorAll( 'a[title*="Tagged"]' ) ) 
				.map( v => new Object( { 
					page: 		v.href, 		// https://derpibooru.org/images/2301306?q=artist%3Ahexado
					title: 			v.href.split( /[\?/]/ )[4], 		// 2301306
					thumb: 	v.querySelector( 'img' ).src, 		// https://derpicdn.net/img/2020/3/19/2301306/thumb.jpg
					image: 	scrub_extensions( v.querySelector( 'img' ).src )
						.replace( '/img/', '/img/view/' )
						.replace( '/thumb', '' )
				} ) )
		}
	break; 

	// https://inkbunny.net/submissionsviewall.php?rid=87f31d45ae&mode=search&page=1&orderby=create_datetime&text=sketches&stringtype=and&keywords=yes&title=yes&description=no&artist=Iztli&favsby=&type=&sale=&days=&keyword_id=&user_id=&random=&md5=
	// Custom thumbnail test - https://inkbunny.net/gallery/atryl/1/734675c046 - they just don't add _noncustom. It's fine. 
	// We could handle single-image submissions via the thumbnail alone. 
	// https://inkbunny.net/submissionsviewall.php?rid=bc5acecefa&mode=search&page=1&orderby=create_datetime&text=lamb+milk&stringtype=and&keywords=yes&title=yes&description=no&artist=zooshi&favsby=&type=&sale=&days=&keyword_id=&user_id=&random=&md5=
		// Only one image. Dammit. 
	case 'inkbunny.net':
		trigger_size = [ 15, 265, 16, 5 ]; 

		html += '<style> span { color: #ddd } </style>';  		// Default colors are grey-on-grey. 

		button_delay_function = () => document.querySelector( '.widget_imageFromSubmission' ); 

		next_page = document.querySelector( 'a[title="next page"' ); 
		next_page = next_page ? next_page.href : null; 
		previous_page = document.querySelector( 'a[title="previous page"' ); 
		previous_page = previous_page ? previous_page.href : null; 

		gather_items = function() { 
			return Array.from( document.querySelectorAll( '.widget_imageFromSubmission' ) ) 
				.map( v => new Object( { 
					page: 		v.querySelector( 'a' ).href,
					title: 			v.querySelector( 'a' ).href.split('/').pop(), 
					thumb: 	v.querySelector( 'img' ).src,
				} ) )
		}

		image_from_dom = function( doc ) {
			let images = Array.from( doc.querySelectorAll( '.widget_imageFromSubmission img[title*="page"]' ) )
				.map( img => scrub_extensions( img.src )
					.replace( 'thumbnails/medium', 'files/full' )
					.replace( '_noncustom', '')
				); 
			if( images.length == 0 ) { 		// Single image
				images = scrub_extensions( doc.querySelector( '.magicboxParent a' ).href ) ; 		// String works, [ string ] doesn't. Hmm. 
			} 
			return images; 
		}
	break;

	// https://www.furaffinity.net/gallery/mab/folder/808215/Divaea 
	case 'furaffinity.net':
		trigger_size = [ 15, 155, 16, 5 ]; 
		formats = [""];

		button_delay_function = () => document.querySelector( 'figure.t-image' ); 

		// https://www.furaffinity.net/scraps/mab/
		// https://www.furaffinity.net/scraps/mab/2/
		// https://www.furaffinity.net/gallery/mab/folder/43380/Wildcard/2/
		// Not even links, on FA - they're form buttons. What year is it? 
		// But then your feed is like:
		// https://www.furaffinity.net/msg/submissions/new~40875552@48/ 
		// And the "next" button is actually 'a.more', except past the first page, where it's 'a.more-half prev'. Argh.
		if( window.location.href.match( 'msg/submissions' ) ) { 
			let buttons = Array.from( document.querySelectorAll( 'a.more-half, a.more' ) ); 
			next_page = buttons.find( v => ! v.className.match( 'prev' ) );
			next_page = next_page ? next_page.href : null;  		// Haaate this pattern. 
			previous_page = buttons.find( v => v.className.match( 'prev' ) );
			previous_page = previous_page ? previous_page.href : null; 
		} else { 
			next_page = Array.from( document.querySelectorAll( 'form' ) ).filter( v => v.innerText.match( 'Next' ) )[0];
			next_page = next_page ? next_page.action : null; 
			previous_page = Array.from( document.querySelectorAll( 'form' ) ).filter( v => v.innerText.match( 'Prev' ) )[0];
			previous_page = previous_page ? previous_page.action : null; 
		} 

		gather_items = function() { 
			let items = Array.from( document.querySelectorAll( 'figure.t-image' ) ) 
				.map( v => new Object( { 
					page: 		v.querySelector( 'a' ).href,
					title: 		v.querySelector( 'a' ).href.split('/')[4],
					thumb: 	v.querySelector( 'img' ).src,
				} ) )
			// Minor witchcraft - some part of FA hits an infinite loop if you replace the page and then resize anything. 
			// I don't understand what exactly it's doing. But I know that removing some things first can break it: 
			Array.from( document.querySelectorAll( 'a' ) ).forEach( v => v.remove() ); 
			return items; 
		}

		image_from_dom = function( doc ) { 
			return doc.querySelector( '.download a' ).href; 
		} 
	break;

	// https://danbooru.donmai.us/posts?tags=marble_macintosh
	// Forced to fetch, because "click for original size" images use a different CDN. 
	// I could try making an educated guess based on data-width and data-large-width in the <article> properties. Meh. 
	case 'danbooru.donmai.us':
		trigger_size = [ 15, 100, 16, 5 ]; 		// Left, top, font-size, padding. 
		formats=[""]; 

		button_delay_function = () => document.querySelector( 'article.post-preview a' ); 

		// https://danbooru.donmai.us/posts?page=2&tags=genshin_impact 
		// href*="=3" sometimes returns https://danbooru.donmai.us/posts?page=218139, because... a wizard did it. 
		let current_page_span = document.querySelector( '.paginator-current' ); 
		next_page = current_page_span.nextElementSibling; 
		next_page = next_page ? next_page.href : null; 
		previous_page = current_page_span.previousElementSibling; 
		previous_page = previous_page ? previous_page.href : null; 

		// https://danbooru.donmai.us/posts?tags=marble_macintosh 
		gather_items = function() { 
			return Array.from( document.querySelectorAll( 'article.post-preview a' ) )
				.map( v => new Object( { 
					page: 		v.href,
					title: 			v.href.split( 'posts/' )[1].split( '?' )[0],
					thumb: 	v.querySelector( 'img' ).src,
				} ) )
		}

		image_from_dom = function( doc ) { 
			return doc.querySelector( 'a[download]' ).href.split('?')[0]; 		// Trim ?download stuff. 
		} 
	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. 
	if( page_number > 0 ) { 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: 	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; color:#194d19 !important; background-color:#dbd7d8; border-radius: 20px; text-align: center; display: inline-block; border:1px solid #19ab19; cursor:pointer; line-height: 20px; font-family:Arial; font-size:" + trigger_size[2] + "px; padding: " + trigger_size[3] + "px " + trigger_size[3] + "px; text-decoration:none;"

add_button = setInterval( function() { 
		if( button_delay_function() ) { 		// Periodically check if this page has what we're looking for. 
			clearInterval( add_button ); 
			document.body.appendChild( trigger ); 
		}  
	}, 100 ); 		// Passive - no interaction concerns. 

// 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[0] ) { 
			clearInterval( fake_event ); 
			show_images(); 
		} 
	}, 100 );  		// Doherty threshold for frustration is 400ms. But: "a couple times a second" doesn't bother modern machines. 

// 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(); } 

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

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

	// Just in case. Fetch traffic might read differently than inline images. 
	if( image_from_dom ) { new_image_rate = fetch_rate; } 

	controls = document.getElementById( 'controls' ); 		// Has to go after document = html, duh.
	thumbnails_container = document.getElementById( 'thumbnails_container' ); 
	centered = document.getElementById( 'centered' ); 
	let links_element = document.getElementById( 'links' ); 

	// Pixiv doesn't work if this function is in the main scope. Every other site is fine. I have no goddamn idea why. 
	// Making the root node an argument and passing #controls doesn't fix the problem. 
	// I've read "programming is a puzzle game where all the puzzles are caused by your own stupidity," but web design comes with its own stupidity DLC. 
	// Is it the function name? Does Pixiv's code have a collision, in the main / global scope? 
	// Yeah apparently. I renamed it to something rude and it worked fine in the main scope. 
	// But... is there a reason not to leave this inside show_images? 
		// Object-oriented Javascript doesn't care. We only add these control buttons, and the initial "swallow gallery" trigger. 
		// Aaaugh alright we also add buttons aplenty per-submission, and will be doing even more for navigation. 
	function add_button( text, onclick ) {  		// DRY. Specific to #controls, for now. 
		let button = document.createElement( 'button' ); 
		button.innerText = text; 
		button.onclick = onclick
		button.className = "control_button eza_button"; 
		controls.appendChild( button ); 
		controls.appendChild( document.createTextNode( " " ) ); 		// Asinine way to force spacing. 
	} 

	// Controls - e.g. image size and order.
	controls.appendChild( document.createTextNode( "Size: " ) );  
	add_button( "▣", function() { centered.className = "short"; fake_css(); } ); 	// This and "full" could use better glyphs.
	add_button( "↔", function() { centered.className = "fit_width"; fake_css(); } ); 
	add_button( "↕", function() { centered.className = "fit_height"; fake_css(); } ); 
	add_button( "✢", function() { centered.className = "fit_window"; fake_css(); } ); 
	add_button( "■", function() { centered.className = "full"; fake_css(); } ); 		// Unicode has no four-way arrow glyph. No emoji either. Weird. 
	controls.appendChild( document.createTextNode( " Order: " ) ); 
	add_button( "⇅", function() { 
//		nav_direction = -1 * nav_direction;  		// Ohhhh. This doesn't matter anymore because we're always in-order. 
		for( node of [ centered, thumbnails_container ] ) {  		// DRY
			node.childNodes.forEach( (v,i,a) => node.insertBefore( node.childNodes[i], node.firstChild ) ); 
		}
	} )

	// Navigation links, at the bottom
	let link_html = ""; 		// "Previous" or "Previous - Next" or "Next"
	// encodeURI breaks links. We're generating text from parseInt page numbers, or grabbing links already in the page - it's fine. 
	if( previous_page ) { link_html += "<a href='" + ( previous_page ) + "'>Previous page</a>"; }  
	if( previous_page && next_page ) { link_html += " - "; }
	if( next_page ) { link_html += "<a href='" + ( next_page ) + "'>Next page</a>"; } 
	links_element.className = 'page_links'; 
	links_element.innerHTML = link_html; 





	// ----- //			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'; 
		container.className = "submission_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 eza_button'; 

		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 navigation_next = document.createElement( 'button' ); 
		navigation_next.innerText = '→'; 
		navigation_next.onclick = function() { scroll_to_next( this, '.submission_container', 1 ); } 
		navigation_next.className = 'navigation_button eza_button'; 

		let navigation_previous = document.createElement( 'button' ); 
		navigation_previous.innerText = '←'; 
		navigation_previous.onclick = function() { scroll_to_next( this, '.submission_container', -1 ); } 
		navigation_previous.className = 'navigation_button eza_button'; 

		let first_spacer = document.createElement( 'button' ); 
		first_spacer.className = 'button_spacer eza_button'; 

		let second_spacer = document.createElement( 'button' ); 
		second_spacer.className = 'button_spacer eza_button'; 

		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 eza_button'; 		// Add some CSS to float left or whatever. 

		let bottom_submission_remover = submission_remover.cloneNode(); 		// Why doesn't this include innerText? 
		bottom_submission_remover.innerText = '✕';  		// ⬆️❌? ↑⇑↥⤒⤊? Eh, nothing color scales well. Obvious mismatch. 
		// 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( navigation_previous ); 
		container.appendChild( first_spacer ); 
		container.appendChild( reloader ); 
		container.appendChild( link ); 
		container.appendChild( submission_remover ); 
		container.appendChild( second_spacer ); 
		container.appendChild( navigation_next ); 
		container.appendChild( document.createElement( 'br' ) ); 
		container.appendChild( basket ); 
		container.appendChild( bottom_submission_remover ); 
		container.appendChild( document.createElement( 'br' ) ); 

		centered.appendChild( container ); 

		// Thumbails at the top: 
		let thumbnail_image = document.createElement( 'img' );
		thumbnail_image.className = "thumbnail_image";  	// Fixed-width image.
		thumbnail_image.src = item.thumb;
		thumbnail_image.onerror = function() { this.remove(); } 

		let thumb_box = document.createElement( 'span' );
		thumb_box.className = "thumb_box"; 						// Fixed-size grid box, overflow hidden. Crops tall images.
		thumb_box.onclick = function() { document.getElementById( item_key + 'container' ).scrollIntoView(); } 
		thumb_box.appendChild( thumbnail_image ); 

		thumbnails_container.appendChild( thumb_box ); 

		fake_css(); 		// Function itself checks force_style boolean. 

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

				// If we need to fetch, we don't have an image to display yet. 
				if( image_from_dom && items[ item_id ].image == null ) { 		// Note: will not re-fetch. Bad item.image value? Tough. 
					standard_fetch( items[ item_id ], ready_element ); 
					return;  		// Exit interval. 
				} 

				let manga_page = parseInt( ready_element.dataset.page_number ); 		// Not web-page... comic-page. 
				ready_element.dataset.page_number = 1 + manga_page; 		// Has to go after fetch stuff, or we skip the first 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 = "full_image loading"; 	
					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", "_generic" ); 		// WHY IS THERE NO OPPOSITE TO DISPLAY:NONE?!

					forms.push( apng ); 
				}

				// Note that we scroll to <div class="image_container> rather than an image. Images scroll to the center. It is the worst. 
				let previous_image = document.createElement( 'button' );
				previous_image.innerText = '←'; 
				previous_image.className = 'navigation_button navigation_float navigation_previous_image eza_button'; 
				previous_image.onclick = function() { scroll_to_next( this, '.image_container', -1 ); } 

				let next_image = document.createElement( 'button' );
				next_image.innerText = '→'; 
				next_image.className = 'navigation_button navigation_float navigation_next_image eza_button'; 
				next_image.onclick = function() { scroll_to_next( this, '.image_container', 1 ); } 

				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 eza_button'; 

				// All per-image divs go in one per-submission span, 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_div = document.createElement( 'div' ); 
				inner_div.style = "position: relative;"; 		// This fixes the vertically-aligned Xs. Do NOT ask me how. 
				inner_div.className = "image_container"; 
				outer_span.appendChild( inner_div ); 

				// Place stuff on the page. 
				inner_div.appendChild( previous_image ); 
				inner_div.appendChild( next_image ); 
				for( form of forms ) { inner_div.appendChild( form ); } 		// Insert all image tries. 
				inner_div.appendChild( remover ); 	
				inner_div.appendChild( document.createElement( 'br' ) ); 
				inner_div.appendChild( document.createElement( 'br' ) ); 



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

					forms = new Array; 
					for( format of formats ) { 
						png = document.createElement( 'img' ); 		// Reuse 
						png.src = items[ item_id ].probe + (manga_page+1) + probe_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_div.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" ); 
					} 
				}

				fake_css( outer_span ); 		// Just for this image and any probes. 

			} 

		}, 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_loading = document.getElementsByClassName( 'ready' );
	submissions_spinner = document.getElementById( 'submissions_spinner' ); 		// This is the blue one. 
	images_loading = document.getElementsByClassName( 'loading' ); 
	images_spinner = document.getElementById( 'images_spinner' ); 					// This is the orange one. 
	image_count = document.getElementsByClassName( 'full_image' ); 
		// 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 */
			if( ! submissions_loading[0] ) { 		// If we're done probing for new images to display.
				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 */
			if( ! images_loading[0] ) { 		// If all visible images are loaded. 
				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 ); 

}