// ==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://pawoo.net/@*
// @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/*
// @include https://www.jabarchives.com/*
// @include https://jabarchives.com/*
// @exclude *#dnr
// @noframes
// @version 2.14.16
// @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 to that image.
// 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; // Set to false if you don't want WASD / QERX / Ctrl+Z to do anything.
var low_opacity = false; // See-through buttons.
// Table of contents:
// Preamble
// CSS
// HTML
// Helper functions
// Main execution
// Per-site image-gathering functions
// The Button
// Show images
// Per-submission setup
// Keyboard controls
// Ongoing image-display function
// Spinners
// Change the BG color to something dark. (Ego says #324.)
// Dark mode button? Seems obvious in retrospect.
// document.body.style.background = "#324"
// The largest obstacle is - where do I put the button? Where isn't it a nuisance? Where doesn't it mess up muscle memory?
// I guess right below the thumbnails. Kind of a dead zone at present.
// #324 with Redshift / f.lux is brown. Hard pass. #E20 is bright red and #E2A is fucking magneta. Just pick a real color.
// 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 are many in number and few in users. An @include block would get ridiculous.
// 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.
// HicceArs?
// Youhate.us? Down.
// https://thehentaiworld.com/tag/romulo-mancin/page/2/ ? Images and manga.
// 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.
// Thematically, the image-size controls should probably be blue. Meh. They're distinct enough for not being grey.
// Consistent color is difficult. I want my clearly red and green reload / remove arrows. But there's little indication of which buttons affect a submission, a single image, the whole page, or simply your position on the page. I tried several janky attempts and figured it's best if engineers don't design interfaces. Plain grey with function-indicative color on-hover is just fine. The only buttons colored by default are the image controls, which are labeled... sort of.
// Abandon thoughts of up-arrow on bottom submission remover. It'd conflict with... actually it might perfetly fit what remove-and-advance conveys.
// Maybe the issue here is overloading what 'X' means. No idea what else I'd use for 'nuke submission.'
// It is annoyingly easy to get caught clicking remove-and-advance instead of just 'advance.'
// The button snaps into place under your cursor if you're submission-aligned and hit 'next image.'
// It's a problem mostly because the apparent result is the same - we scroll clean past the removed image.
// I could move html+="<text>" into actual DOM stuff by doing replacement_html = document.createElement( 'html' ).
// Main reason not to: it's so much more verbose. Even if I moved append_new_element to the top and used that: text is concise.
// Apparently you can just reference elements in "window" by their ID. It's not recommended practice... but oh well.
// Add syntactical sugar by replacing getElementById('name') with name_id.
// ... is it a security concern? We're just throwing shit at the page. We don't read anything from whatever_id.
// We click() global_undo_id, so I guess give that a getElementsById out of sheer paranoia.
// Do remember these are third-party webpages. If they wanted to put malicious crap in your browser... it'd be there.
// And: they already have defenses against users injecting arbitrary HTML.
// Out-of-bounds signalling for page count -might- still be useful for Pixiv. That'd let us skip the thumbnail-try stuff.
// To do:
// Put alt/title (onhover) text on size / order buttons?
// Ayrion.
// Genuine bugs:
// Dead images and possibly thumbnails take up horizontal space and cause slight movements while loading.
// Spinners don't spin on Baraag. (Might be intractable.)
// Keyboard controls often can't remove the last image. Eh. (Could give it 100vh of runout.)
// https://pawoo.net/@niwa2eito8/media?page=50 only shows "next page" when it should say "last page."
// Changes since last version:
// Added bottom anchor for keyboard controls. You can now browse past the last image.
// Optional-ish low-opacity buttons.
// Jab Archives: sorted for consistent chronological order. (Individual folders can go either way. Might change back, since it's unexpected behavior.)
// Filtered folder links from appearing as images.
// Modified button_delay_function to look for /post links instead of /gallery links.
// Golfed reverse-order button into generic "reversible" class.
// Gave image-size buttons "radio button" highlights.
// Tried actual radio buttons - not worth the hassle. (Styling fake buttons is not concise, and we need onclick anyway.)
// Replaced getElementById( 'thing_id' ) with references to thing_id. Possibly unwise.
// The one place we thing_id.click instead of throwing appendChild at it, I changed it to getElement, out of abundant caution.
// Renamed images_loader and submissions_loader classes to blue/orange_spinner because that was needlessly confusing.
// Added really clean and simple dark mode.
// Swapped that for slight overkill because enforce_style makes us fight the CSS even when CSS works sensibly. God dammit.
// Fixed slight gap at the bottom, because bottom_anchor appended to document.body instead of my goofy background div.
// Dorked with Pixiv: concat users/bookmarks lists, treat /series separately.
// ------------------------------------ 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; vertical-align: middle;"; // Class is for <body>.
style_rules[ ".full img" ] = "max-width: initial; max-height: initial; vertical-align: middle;"; // "initial" for enforce_style().
style_rules[ ".fit_width img" ] = "max-width: 90vw; max-height: initial; vertical-align: middle;"; // Leaving space for big X.
style_rules[ ".fit_height img" ] = "max-width: initial; max-height: 95vh; vertical-align: middle;";
style_rules[ ".fit_window img" ] = "max-width: 90vw; max-height: 95vh;vertical-align: middle;";
// Dead spinners and other invisible elements:
style_rules[ '.invisible' ] = 'display: none;'; // Also used for hidden probe thumbnails.
style_rules[ '#image_counter_id' ] = 'position: absolute; left: 70px; top: 5px; font-size: 33px;';
style_rules[ '.button_spacer' ] = 'visibility: hidden; width: 30px !important; height: 30px;';
// 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;';
if( low_opacity ) { style_rules[ '.eza_button' ] += ' opacity: 0.3;'; style_rules[ '.eza_button:hover' ] = 'opacity: 1.0;'; } // Maybe.
// 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' ] = '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;';
style_rules[ '.undo' ] = 'position: fixed; right: 0px; bottom: 0px;';
// Previous / next buttons, per-submission:
style_rules[ '.nav_button' ] = 'color:#123 !important; background-color:#d7dbd8; border:1px solid #234;';
style_rules[ '.nav_button:hover' ] = 'background-color:#14f;';
style_rules[ '.nav_float' ] = 'z-index: 11; position: absolute; left: 0px; top: initial;';
style_rules[ '.nav_next_image' ] = 'top: 72px;';
style_rules[ '.remove_and_advance' ] = 'top: 144px; font-size: 20px !important; color: #a12 !important';
style_rules[ '.remove_and_advance:hover' ] = 'background-color:#dd2e44;';
// 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;';
style_rules[ '.control_button.active' ] = 'color:#FFF; background-color:#191;';
// Dark mode button:
style_rules[ '#backdrop_id.dark_background' ] = 'background: #181818';
style_rules[ '#backdrop_id.disappear' ] = 'background: rgba(0,0,0,0)'; // For enforce_style.
style_rules[ '.dark_background #dark_mode_button' ] = 'color: #000;'
// Previous / next links, at the bottom:
style_rules[ '#links_id' ] = 'font-size: 33px; !important';
// Thumbnail container and fixed-size thumbnails:
style_rules[ '.thumbnails' ] = 'display: grid; grid-gap: 5px;'; // Spans can't have fixed size, divs can't flow sensibly. Argh.
style_rules[ '#thumbnails_id span' ] = 'width:100px; height:100px; display: inline-block; overflow: hidden;';
style_rules[ '#thumbnails_id img' ] = 'width:100%;';
style_rules[ 'body' ] = 'line-height: 1.425 !important'; // Low values don't separate thumbnails from image controls. Gelbooru picks 1.42857143... for some reason.
// Spinners to indicate loading submissions / loading images:
style_rules[ '.blue_spinner' ] = 'position: absolute; left: 0px; top: 0px; z-index: 10; border: 8px solid #3498db; border-top: 8px solid #111111; border-bottom: 8px solid #111111; border-radius: 50%; width: 48px; height: 48px; box-sizing: unset !important; animation: spin 1s linear infinite;'
style_rules [ '.orange_spinner' ] = 'position: absolute; left: 8px; top: 8px; z-index: 9; border: 24px solid #db9834; border-top: 24px solid #aaaaaa; border-bottom: 24px solid #AAAAAA; border-radius: 50%; width: 0px; height: 0px; box-sizing: unset !important; animation: images_spin 3s linear infinite;'
// Push all of that into a <style> block:
html += '<style> ';
for( selector in style_rules ) { html += selector + ' { ' + style_rules[ selector ] + ' } \n'; }
html += '</style> ';
// Spinner animations kinda shit the bed when you querySelector( '@keyframes spin' ).
html += '<style> @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } </style>';
html += '<style> @keyframes images_spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } </style>';
html += '<style> @keyframes fadeout { from { opacity: 1; } to { opacity: 0; } } </style>';
html += '<style> #style_check_id{ display: none; } </style> <div style="position: absolute;" id="style_check_id"> </div>'; // Invisible probe for enforce_style.
// ----- // Page elements
// Put everything in one DIV so I can change the background color. Yes, this is completely necessary.
html += '<div id="backdrop_id" class="disappear">';
// Floaty stuff:
html += '<span id="spinners_id">'; // So enforce_style() can apply to spinners. Sort of.
html += '<div class="blue_spinner" id="submissions_spinner_id"></div>' // Spinner for submissions, 'new images being found.' Blue.
html += '<div class="orange_spinner" id="images_spinner_id"></div>' // Spinner for images, 'images loading in high-res.' Orange.
html += '<span id="image_counter_id"></span>';
html += '</span>';
// Structure:
html += '<br><span id="controls" class=""></span><br><br><br><br>'; // Image-size controls.
html += '<span id="thumbnails_id"></span><br><br>';
html += '<br><center><span id="dark_mode_id"></span></center><br>'; // <span/> is deprecated.
html += '<br><center><span id="centered_id" class="' + default_size + '"></span></center>'; // Where most stuff goes.
html += '<br><br><br>'; // Spacing for prev / next links.
html += '<center><span id="links_id"></span></center>'; // Previous Page / Next Page.
html += '<br><br><br><br><br>'; // Runout.
html += '</div>';
// 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 next_page, previous_page; // URLS to the actual previous / next page (where applicable).
var undo_list = new Array; // For holding and restoring elements. (See undoable_replace.) First in, last out.
// ----- // Helper functions
// Things specific to this script and its global variables:
// Replace .jpg, .png, etc., because full-size image formats don't always match their thumbnail format.
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, '%format' ) )
return url;
}
// Fake CSS with inline style, if in-page CSS is prevented by CSP. I hate the modern web.
function enforce_style( parent ) {
if( style_check_id.clientWidth == 0 ) { return; } // Inline <style> block, not in style_rules.
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, flag image(s) as ready to display.
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.text();
} )
.then( text => {
doc = document.createElement( 'html' );
doc.innerHTML = text;
item_object.image = image_from_dom( doc );
span.classList.add( "ready" );
} )
}
// Grab element, swap it out, and keep both, so they can be swapped back.
// Format is an array of objects, with properties .original and .dummy, both Elements.
function undoable_replace( current, replacement ) {
if( ! current.className.contains( 'basket' ) ) // When removing images, mark submission as modified.
{ current.closest( '.submission' ).querySelector( '.basket' ).classList.remove( 'unmodified' ) }
current.replaceWith( replacement );
undo_list.push( new Object( { original: current, dummy: replacement } ) );
return undo_list[ undo_list.length - 1 ];
}
// Things that really ought to be trivial and standard, but are a pain in the ass:
// Turn window.location.search into a sensible associative array. Should be standard!
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;
}
// Concise, one-step createElement / set attributes / appendChild pattern.
// Appending is odd, but createElement( 'span', {id:'whatever'} ) should be a standard option.
function append_new_element( target, nodename, attribute_object ) {
let element = document.createElement( nodename );
for( attribute in attribute_object ) { element[ attribute ] = attribute_object[ attribute ]; }
target.appendChild( element );
return element;
}
// Return the next / previous instance of a selector, relative to a given element.
// I tried doing this as Element.prototype.nextQuery and none of the sites were having it.
function next_element( origin, selector, direction ) {
if( direction == null ) { direction = 1; }
let reference = origin.closest( selector ); // Ancestor or self, whatever. Pass in "this" for the origin and don't worry.
let list = Array.from( document.querySelectorAll( selector ) );
let index = list.findIndex( e => e == reference );
return list[ index + direction ];
}
function previous_element( origin, selector ) {
return next_element( origin, selector, -1 );
}
// TypeErrors for getElement / querySelector() .href are aggravating bullshit.
function safely_return_property( object, property ) {
return object ? object[ property ] : null;
}
// Sensible "are we on this site or not?!" function.
function domain( ending ) {
let want = ending.split( '.' ).reverse(); // Reverse order, from TLD to domain to subdomain(s).
let have = document.domain.split( '.' ).reverse();
for( let n = 0; n < want.length; n++ ) {
if( want[n] != have[n] ) { return false; }
} // Implicit else
return true;
}
// ----- // Per-site setup and gather functions
var args = parse_search( window.location.search ); // DRY
if( domain( 'pixiv.net' ) ) {
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?p=2
// Did a lot of remove-empty-container and undo testing here: https://www.pixiv.net/en/users/45227836/artworks
button_delay_function = () =>
document.querySelector( 'img[src*="360x"]' ) || // /series
document.querySelector( 'img[src*="250x"]' ) || // /users
document.querySelector( 'a *[style*="c/240"]' ); // bookmark_new_illust
gather_items = function() {
args = parse_search( window.location.search ); // Fake page transitions mean this script doesn't restart, so... do this again.
let page_number = args["p"] ? parseInt( args["p"] ) : 1;
if( page_number > 1 ) { previous_page = window.location.origin + window.location.pathname + "?p=" + (page_number - 1); }
next_page = window.location.origin + window.location.pathname + "?p=" + (page_number + 1)
// https://i.pximg.net/c/360x360_70/img-master/img/2020/06/20/11/14/47/82439841_p0_square1200.jpg
let thumbs = Array.from( document.querySelectorAll( 'a[href*="/art"] img[src*="c/360"]' ) ) // /series
.map( img => img.src );
if( thumbs.length == 0 ) { // We'd concat() all three, but /series pages have spurious 250x250 links on the side.
// https://i.pximg.net/c/250x250_80_a2/img-master/img/2020/05/15/04/50/43/81571620_p0_square1200.jpg
thumbs = Array.from( document.querySelectorAll( 'a[href*="/art"] img[src*="c/250"]' ) ) // en/users/12345
.map( img => img.src )
// url("https://i.pximg.net/c/240x240/img-master/img/2021/02/17/04/05/41/87838098_p0_master1200.jpg")
.concat( 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 go 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 + '%number' + '_square1200' + '%format',
image: 'https://i.pximg.net/img-original/img/' + image_part + '%number' + '%format'
} )
} )
}
}
if( domain( 'gelbooru.com' ) || domain( 'safebooru.org' ) ) {
// https://gelbooru.com/index.php?page=post&s=list&tags=shuujin_academy_uniform+chair+1girl+pink_background
// 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.
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.
let page_number = args["pid"] ? parseInt( args["pid"] ) : 0;
let 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( page_number > 1 ) { previous_page = window.location.href + "&pid=" + (page_number - pages_at_once); }
next_page = window.location.href + "&pid=" + (page_number + pages_at_once); // Flawed, but it works.
gather_items = () => 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?3389404
thumb: v.querySelector( 'img' ).src,
image: scrub_extensions( v.querySelector( 'img' ).src )
.replace( '/thumbnails', '/images' )
.replace( 'thumbnail_', '' )
.replace( '_thumbnail', '' ) // Safebooru
} ) )
}
if( domain( 'e621.net' ) || domain( '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; }
previous_page = safely_return_property( document.querySelector( '#paginator-prev' ), 'href' );
next_page = safely_return_property( document.querySelector( '#paginator-next' ), 'href' );
gather_items = () => 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', '' )
} ) )
}
if( domain( 'hentai-foundry.com' ) ) {
// http://www.hentai-foundry.com/users/FaveUsersRecentPictures?username=AmaZima
// http://www.hentai-foundry.com/user/InCase/faves/pictures
// http://www.hentai-foundry.com/pictures/user/AmaZima
// http://www.hentai-foundry.com/pictures/user/AmaZima/scraps - single image, no pages
// Haha, http://www.hentai-foundry.com/pictures/user/Underrock/883176/MTWD-Ch.-II-Pg.-16#dnr#&dnr shows both PNG & JPG.
var trigger_size = [ 15, 20, 25, 10 ];
previous_page = safely_return_property( document.querySelector( 'li.previous a' ), 'href' );
next_page = safely_return_property( document.querySelector( 'li.next a' ), 'href' );
button_delay_function = () => document.querySelector( 'a.thumbLink' )
gather_items = () => 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, '_' )
+ '%format'
} )
} )
}
// Baraag... and Mastodon in general, ideally.
if( domain( 'baraag.net' ) ||
domain( 'equestria.social' ) ||
domain( 'botsin.space' ) ||
domain( 'pawoo.net' ) ||
domain( 'mastodon.art' ) ||
domain( 'mastodon.social' ) ) {
formats = [""];
// 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] ) { // Always 0, 1, or 2 elements long.
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 = () => 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;
} )
}
if( domain( 'rule34.paheal.net' ) ) {
// http://rule34.paheal.net/post/list/Marco_Diaz%20Polyle/1
formats = [""]; // Direct image links are right there on the page.
previous_page = safely_return_property( Array.from( document.querySelectorAll( 'a' ) ).find( a => a.innerText.match( 'Prev' ) ), 'href' );
next_page = safely_return_property( Array.from( document.querySelectorAll( 'a' ) ).find( a => a.innerText.match( 'Next' ) ), 'href' );
button_delay_function = () => document.querySelector( '.shm-thumb' );
gather_items = () => 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 )
} ) )
}
if( domain( 'rule34.xxx' ) ) {
// https://rule34.xxx/index.php?page=post&s=list&tags=davepetasprite%5e2+
trigger_size = [ 150, 5, 16, 5 ];
previous_page = safely_return_property( document.querySelector( 'a[alt="back"' ), 'href' );
next_page = safely_return_property( document.querySelector( 'a[alt="next"' ), 'href' );
button_delay_function = () => document.querySelector( '.thumb' );
gather_items = () => Array.from( document.querySelectorAll( '.thumb' ) )
.map( v => new Object( {
page: v.querySelector( 'a' ).href,
title: v.id,
// https://miami.rule34.xxx/thumbnails/3500/thumbnail_62b3adfaa4d100c4a6fc7d419f61dd49.jpg?3944961
// https://us.rule34.xxx//images/3500/62b3adfaa4d100c4a6fc7d419f61dd49.png?3944961
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_', '' )
} ) )
}
if( domain( 'derpibooru.org' ) ) {
// 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.
trigger_size = [ 15, 55, 16, 5 ]; // Left, top, font-size, padding.
previous_page = safely_return_property( document.querySelector( 'a.js-prev' ), 'href' );
next_page = safely_return_property( document.querySelector( 'a.js-next' ), 'href' );
button_delay_function = () => document.querySelector( 'a[title*="Tagged"]' );
gather_items = () => 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', '' )
} ) )
}
if( domain( 'inkbunny.net' ) ) {
// 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.
// This seems to mess with your preview size settings.
trigger_size = [ 15, 265, 16, 5 ];
previous_page = safely_return_property( document.querySelector( 'a[title="previous page"' ), 'href' );
next_page = safely_return_property( document.querySelector( 'a[title="next page"' ), 'href' );
html += '<style> span { color: #ddd } </style>'; // Default colors are grey-on-grey.
button_delay_function = () => document.querySelector( '.widget_imageFromSubmission' );
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;
}
}
// https://www.furaffinity.net/view/41094211/#dnr#&dnr ?
if( domain( 'furaffinity.net' ) ) {
// https://www.furaffinity.net/gallery/mab/folder/808215/Divaea
trigger_size = [ 15, 155, 16, 5 ];
formats = [""];
// 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' ) );
previous_page = safely_return_property( buttons.find( v => v.className.match( 'prev' ) ), 'href' );
next_page = safely_return_property( buttons.find( v => ! v.className.match( 'prev' ) ), 'href' );
} else {
previous_page = safely_return_property( Array.from( document.querySelectorAll( 'form' ) )
.find( v => v.innerText.match( 'Prev' ) ), 'action' );
next_page = safely_return_property( Array.from( document.querySelectorAll( 'form' ) )
.find( v => v.innerText.match( 'Next' ) ), 'action' );
}
button_delay_function = () => document.querySelector( 'figure.t-image' );
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;
}
}
if( domain( 'danbooru.donmai.us' ) ) {
// 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.
trigger_size = [ 15, 100, 16, 5 ]; // Left, top, font-size, padding.
formats=[""];
previous_page = safely_return_property( document.querySelector( '.paginator-prev' ), 'href' );
next_page = safely_return_property( document.querySelector( '.paginator-next' ), 'href' );
button_delay_function = () => document.querySelector( 'article.post-preview a' );
gather_items = () => 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.
}
}
if( domain( 'booru.org' ) ) {
// https://svtfoe.booru.org/index.php?page=post&s=list&tags=socks&pid=20
trigger_size = [ 15, 55, 16, 5 ]; // Left, top, font-size, padding.
previous_page = safely_return_property( document.querySelector( 'a[alt="back"]' ), 'href' );
next_page = safely_return_property( document.querySelector( 'a[alt="next"]' ), 'href' );
gather_items = () => 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_', '' )
} ) )
}
if( domain( 'jabarchives.com' ) ) {
trigger_size = [ 90, 55, 16, 5 ]; // Left, top, font-size, padding.
// Two identical .pagination bars. So: "previous" is before the active page in the first bar, and "next" is after the one in the second bar.
let this_link = document.querySelectorAll( 'li.page-item.active a' )
if( this_link[0] ) { previous_page = safely_return_property( previous_element( this_link[0], 'li.page-item a' ), 'href' ); }
if( this_link[1] ) { next_page = safely_return_property( next_element( this_link[1], 'li.page-item a' ), 'href' ); }
button_delay_function = () => document.querySelector( 'li.gallrpli a[href*="/post"]' ); // Don't match /gallery links.
gather_items = () => Array.from( document.querySelectorAll( 'li.gallrpli img' ) )
.sort( (x,y) => parseInt( x.closest( 'a' ).href.split('/').pop() ) < parseInt( y.closest( 'a' ).href.split('/').pop() ) ) // Forced chronological order. Iffy.
.filter( v => ! v.closest( 'a' ).href.match( '/gallery' ) )
.map( v => new Object( {
// https://www.jabarchives.com/main/gallery/misterd/105
page: v.closest( 'a' ).href,
title: v.closest( 'a' ).href.split('/').pop(),
// https://www.jabarchives.com/main/media/posts/2019/06/13/Z9EQ5L41560468693163192331331_thumb.png
// https://www.jabarchives.com/main/media/posts/2019/06/13/Z9EQ5L41560468693163192331331_large.png
thumb: v.src,
image: scrub_extensions( v.src )
.replace( '_thumb', '_large' )
} ) )
}
// ----- // 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.
var trigger = document.createElement( 'button' );
trigger.innerText = "Swallow gallery";
trigger.className = "unclicked_button";
trigger.onclick = function() { this.innerText='Swallowing...'; show_images(); } // Oh. I didn't know I could just call that from here.
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.
// 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.
// items = []; // Debug aid for checking previous page / next page.
// Erase existing page, use ours instead
document.body.innerHTML = html;
// Just in case. Fetch traffic might read differently than inline images.
if( image_from_dom ) { new_image_rate = fetch_rate; }
function add_size_button( text, name ) { // DRY for six nearly-identical buttons.
append_new_element( controls, 'button', { innerText: text, className: "control_button eza_button", id: name,
onclick: function () {
centered_id.className = this.id;
document.querySelectorAll( '.control_button' ).forEach( e => e.classList.remove( 'active' ) );
this.classList.add( 'active' );
enforce_style();
}
} );
controls.appendChild( document.createTextNode( " " ) ); // Asinine way to force spacing.
}
// Controls - e.g. image size and order.
// Using actual radio buttons doesn't save space or complexity, because we need labels that look like buttons, and call onclick anyway.
// Maybe [ [ "▣", "short" ]. [ "↔", "fit_width" ] ].forEach? Eh. It becomes a readability issue.
// Saves lines by not really having an in-scope add-a-button class.
// I can't use "■" as the name for an object property. I'm honestly kinda relieved.
controls.appendChild( document.createTextNode( "Size: " ) );
add_size_button( "▣", "short" );
add_size_button( "↔", "fit_width" );
add_size_button( "↕", "fit_height" );
add_size_button( "✢", "fit_window" );
add_size_button( "■", "full" );
document.getElementById( default_size ).click(); // DRY for highlighting the active size.
controls.appendChild( document.createTextNode( " Order: " ) );
append_new_element( controls, 'button', { className: "control_button eza_button", innerText: '⇅',
onclick: function() { document.querySelectorAll( '.reversible' )
.forEach( node => node.parentElement.insertBefore( node, node.parentElement.firstChild ) )
} } );
// Navigation links, at the bottom.
let link_html = ""; // "Previous" or "Previous - Next" or "Next"
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_id.innerHTML = link_html;
// Global undo button, pinned to the corner.
let global_undo = append_new_element( document.body, 'button', { innerText: '⟲',
className: 'reloader undo eza_button', id: 'global_undo_id',
onclick: function() {
let element = undo_list.pop();
element.dummy.replaceWith( element.original );
element.dummy.remove(); // Fingers crossed this frees the memory. Not super important.
element.original.scrollIntoView();
} } );
// Dark mode button.
append_new_element( dark_mode_id, 'button', { className: 'eza_button control_button', id: 'dark_mode_button', innerText: '✺',
onclick: function() {
backdrop_id.classList.toggle( 'dark_background' );
backdrop_id.classList.toggle( 'disappear' ); // Contrapositive, to appease enforce_style.
enforce_style();
}
} )
// ----- // Per-submission links and controls
// Give each item its own set of spans, with basic onClick controls to remove images or reload a submission.
// Very few of these need to be variables now, but 'let purpose =' adds clarity. It's no less efficient than before.
for( let item_key = 0; item_key < items.length; item_key++ ) {
item = items[ item_key ];
let container = append_new_element( centered_id, 'div',
{ id: item_key + 'container', className: "submission reversible" } )
// ← ⟳ 12345 ✕ →
let nav_previous = append_new_element( container, 'button', { className: 'nav_button previous eza_button', innerText: '←',
onclick: function() { previous_element( this, '.submission' ).scrollIntoView(); } } );
let first_spacer = append_new_element( container, 'button', { className: 'button_spacer eza_button' } );
let reloader = append_new_element( container, 'button', { className: 'reloader eza_button', innerText: '⟳',
onclick: function() {
let pair = undoable_replace( this.closest( '.submission' ).querySelector( '.basket' ),
append_new_element( document.body, 'span', { id: item_key, className: 'ready basket unmodified' } ) );
// If the old basket was unchanged or nonexistent, don't keep its "undo" state.
if( pair.original.classList.contains( 'initialize' ) || pair.original.classList.contains( 'unmodified' ) ) { undo_list.pop(); }
} } );
let link = append_new_element( container, 'a', { href: item.page + '#dnr#&dnr',
innerText: ' ' + item.title + ' ', target: '_blank', style: 'font-size:30px' } ); // Only "code smell" says this should go in CSS.
let submission_remover = append_new_element( container, 'button', { className: 'remover top eza_button', innerText: '✕',
onclick: function(){
if( this.closest( '.submission' ).querySelector( '.image_container' ) == null ) { return; } // Don't remove (and store) empty submissions.
undoable_replace( this.closest( '.submission' ).querySelector( '.sub_basket' ), document.createElement( 'span' ) );
} } );
let second_space = append_new_element( container, 'button', { className: 'button_spacer eza_button' } );
let nav_next = append_new_element( container, 'button', { className: 'nav_button next eza_button', innerText: '→',
onclick: function() { next_element( this, '.submission' ).scrollIntoView(); } } );
append_new_element( container, 'br' );
// Image(s) and the ✕ below each group.
let basket = append_new_element( container, 'span', { id: item_key, className: 'basket initialize' } ); // Hold off on ready-ing.
let bottom_submission_remover = append_new_element( container, 'button', { className: 'remover bottom eza_button', innerText: '✕',
onclick: function() {
let sub = this.closest( '.submission' );
sub.querySelector( '.remover.top' ).click(); // Click top X for identical behavior.
sub.scrollIntoView(); // Scroll back up, since vertical content disappeared.
} } );
// Thumbails at the top. Fixed-size grid box, overflow hidden. Crops tall images.
let thumb_box = append_new_element( thumbnails_id, 'span', { className: 'reversible',
onclick: function() { document.getElementById( item_key + 'container' ).scrollIntoView(); } } )
let thumbnail_image = append_new_element( thumb_box, 'img', { src: item.thumb } );
enforce_style(); // Applies CSS per-element, if it needs to.
reloader.click(); // DRY
}
// ----- // Keyboard controls
if( keyboard_controls ) {
// Hmmmm I should probably still addEventListener for scrolling and highlight the current image somehow. Control opacity? Image border?
// The real issue is that current_image and current_submission don't have to line up.
document.addEventListener( "keydown", key_handler, false );
function key_handler( event ) {
// Find "current" elements. I.e., which image / submission is onscreen. (A fuzzy question.)
current_image = Array.from( document.querySelectorAll( '.image_container' ) )
.find( image => image.getBoundingClientRect().top + image.scrollHeight > 100 ); // > image.scrollHeight * 0.5?
if( ! current_image ) { current_image = Array.from( document.querySelectorAll( '.image_container' ) ).pop(); }
// Scrolling past the last image can cause no selection, so pick the last image.
current_submission = Array.from( document.querySelectorAll( '.submission' ) )
.find( sub => sub.getBoundingClientRect().top + sub.scrollHeight > 75 ); // > window.screenY / 2?
// 100 seems a bit fucky with multiple failed-image posts?
// E.g. https://pawoo.net/@blooddj/media?page=24 circa 103169897373770181.
if( ! current_submission ) { current_submission = Array.from( document.querySelectorAll( '.submission' ) ).pop(); }
if( event.key == 'a' ) { current_image.querySelector( '.nav_previous_image' ).click(); }
if( event.key == 'd' ) { current_image.querySelector( '.nav_next_image' ).click(); }
if( event.key == 'q' ) { current_image.querySelector( '.remover.floating' ).click(); }
if( event.key == 'e' ) { current_image.querySelector( '.remove_and_advance' ).click(); }
if( event.key == 'w' ) { current_submission.querySelector( '.previous' ).click(); }
if( event.key == 's' ) { current_submission.querySelector( '.next' ).click(); }
if( event.key == 'r' ) { current_submission.querySelector( '.reloader' ).click(); }
if( event.key == 'x' ) { current_submission.querySelector( '.remover.bottom' ).click(); }
if( event.key == 'z' ) { document.getElementById( 'global_undo_id' ).click(); } // Ctrl+Z also works.
}
// Alright, make it so hitting 'next' on a freshly-loaded page goes to the first image instead of the second.
// Can't just append two buttons of className x_container, because thing.querySelector can't return 'thing.'
let top_anchor = append_new_element( spinners_id, 'div', { className: 'image_container submission',
style: 'position: absolute; top: 0px; left: 0px; height: 300px; visibility: hidden;' } ); // display:none breaks scrollIntoView.
let first_image = append_new_element( top_anchor, 'button', { className: 'nav_next_image remove_and_advance',
onclick: function() { next_element( this, '.image_container' ).scrollIntoView(); } } );
let first_submission = append_new_element( top_anchor, 'button', { className: 'next',
onclick: function() { next_element( this, '.submission' ).scrollIntoView(); } } );
// Okay fine there should be a bottom anchor too.
let bottom_anchor = append_new_element( backdrop_id, 'div', { className: 'image_container submission',
style: 'height: 100px; visibility: hidden;' } ); // display:none breaks scrollIntoView.
let final_image = append_new_element( bottom_anchor, 'button', { className: 'nav_previous_image',
onclick: function() { previous_element( this, '.image_container' ).scrollIntoView(); } } );
let final_submission = append_new_element( bottom_anchor, 'button', { className: 'previous',
onclick: function() { previous_element( this, '.submission' ).scrollIntoView(); } } );
}
// ----- // Ongoing interaction and loading
// Load more images in "ready" submissions, when available.
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 = document.querySelector( '.ready' ) ) {
ready_element.classList.remove( 'ready' ); // We fancy.
let item_id = parseInt( ready_element.id ); // This should maybe be data-id or something.
// 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.
}
if( isNaN( ready_element.dataset.image_number ) ) { ready_element.dataset.image_number = 0; } // No initial value required.
let manga_page = parseInt( ready_element.dataset.image_number ); // Not web-page... comic-page.
ready_element.dataset.image_number = 1 + manga_page; // Has to go after fetch stuff, or we skip the first page.
// All per-image divs go in one per-submission span, so whole-submission X prevents new images from loading.
// Not 100% sure this stops anything from loading, now that "undo" mans the basket still exists... somewhere. Seems like a solid "mostly."
let outer_span = ready_element.querySelector( '.sub_basket' );
if( outer_span == null ) { outer_span = append_new_element( ready_element, 'span', { className: 'sub_basket' } ); } // Two hard problems.
let inner_div = append_new_element( outer_span, 'div', { style: 'position: relative;', className: 'image_container' } );
// Place stuff on the page.
let previous_image = append_new_element( inner_div, 'button', { innerText: '←',
className: 'nav_button nav_float nav_previous_image eza_button',
onclick: function() { previous_element( this, '.image_container' ).scrollIntoView(); } } );
let next_image = append_new_element( inner_div, 'button', { innerText: '→',
className: 'nav_button nav_float nav_next_image eza_button',
onclick: function() { next_element( this, '.image_container' ).scrollIntoView(); } } );
let remove_and_advance = append_new_element( inner_div, 'button', { innerText: '✕→',
className: 'remover nav_button remove_and_advance nav_float eza_button',
onclick: function() {
let next = next_element( this, '.image_container' );
this.closest( '.image_container' ).querySelector( '.remover.floating' ).click(); // DRY - click other remove button.
next.scrollIntoView();
let toaster = append_new_element( document.body, 'span', { innerText: '✕ Image removed',
style: 'position: fixed; left: 60px; top: 0px; animation: fadeout 3s;' } )
setTimeout( () => { toaster.remove(); }, 3000 );
} } );
// The actual image. Or, several attempts to find the actual image, each with different file extensions.
let image_url = items[ item_id ].image; // Prepare filename once.
if( typeof( items[ item_id ].image ) == "object" ) { image_url = items[ item_id ].image[ manga_page ]; }
if( ! image_url ) { image_url = ''; } // Getting an Undefined or Null makes the script stumble. Clunky debug-ish fix.
if( automatic_pagination ) { image_url = image_url.replace( '%number', manga_page ) } // Basically just Pixiv.
for( format of formats ) { // Try all plausible file extensions for an inline image.
// console.log( image_url ); // Debug.
let apng = append_new_element( inner_div, 'a', { href: image_url.replace( '%format', format ), target: '_generic' } );
append_new_element( apng, 'img', { src: image_url.replace( '%format', format ), className: 'loading',
onerror: function() {
let container = this.closest( '.image_container' ); // If all images disappear, remove the container.
this.closest( 'a' ).remove(); // Also destroys "this".
setTimeout( () => { if( ! container.querySelector( 'img' ) ) { container.remove(); } }, 1000 ); // Goddamn race conditions.
}, // Remove parent link as well.
onload: function() { this.classList.remove( "loading" ); } } );
}
let remover = append_new_element( inner_div, 'button', { innerText: '✕', className: 'remover floating eza_button',
onclick: function() {
undoable_replace( this.closest( '.image_container' ), document.createElement( 'span' ) );
} } ); // Newline format purely to avoid "));}});".
append_new_element( inner_div, 'br' );
append_new_element( inner_div, 'br' );
// Probe-try stuff:
if( automatic_pagination ) {
for( format of formats ) {
append_new_element( inner_div, 'img', { className: 'invisible',
src: items[ item_id ].probe.replace( '%format', format ).replace( '%number', (manga_page+1) ),
onerror: function() { this.remove(); },
onload: function() { this.closest( '.basket' ).classList.add( "ready" ); this.remove(); } } )
}
}
// Array-of-images stuff:
if( typeof( items[ item_id ].image ) == "object" ) { // Direct testing beats keeping track.
if( manga_page + 1 < items[ item_id ].image.length ) {
ready_element.classList.add( "ready" );
}
}
enforce_style( outer_span ); // Just for this image and any probes.
}
}, new_image_rate );
// ----- // 🍭 Spinners 🍭
// Garish spinners that indicate "finding new images" and "files still loading."
// Barring a CSS :has property (CSS4 / ES22, maybe,) it is impossible to style an element based on its children. So the JS stays.
// We could probably add a spinner to each image_container, and give it absolute position in the window... maybe.
// That would be massively redundant, to the point I'd worry about performance, and it'd look exactly the same as this.
// Unless you made each 'spinner' a slowly rotating stick, and they each started when the image appeared, so you got a 'fascinator' look. Eh.
// Easy answer for responsive loading / lazy spinners: use another interval.
// Hoisted global variables caused weird behavior, and getElementsByClassName[0] has fuck-all performance impact.
var spinner_interval = setInterval( function() {
submissions_spinner_id.className = 'blue_spinner'; // While submissions are still adding images.
if( ! document.getElementsByClassName( 'ready' )[0] ) { submissions_spinner_id.className = 'invisible'; }
images_spinner_id.className = 'orange_spinner'; // While images are still downloading.
if( ! document.getElementsByClassName( 'loading' )[0] ) { images_spinner_id.className = 'invisible'; }
image_counter_id.innerText =
'' + ( document.getElementsByClassName( 'image_container' ).length - 1 ) + ' images'; // -1 for top_anchor.
enforce_style( spinners_id );
}, 1000 );
}