Keybinds for navigating boorus
Od
// ==UserScript==
// @name booru keybinds
// @namespace 861ddd094884eac5bea7a3b12e074f34
// @version 3.2
// @description Keybinds for navigating boorus
// @author Anonymous
// @match https://rule34.xxx/index.php?page=post&s=list*
// @match https://rule34.xxx/index.php?page=favorites&s=view&id=*
// @match https://yande.re/post
// @match https://yande.re/post?tags=*
// @match https://yande.re/post?page=*
// @match https://rule34.us/index.php?r=posts/index*
// @match https://rule34.us/index.php?r=posts%2Findex*
// @match https://rule34.us/index.php?r=favorites/view*
// @match https://rule34.us/index.php?r=favorites%2Fview*
// @exclude https://rule34.us/index.php?r=posts/view*
// @match https://gelbooru.com/index.php?page=post&s=list*
// @match https://gelbooru.com/index.php?page=favorites&s=view&id=*
// @match https://www.zerochan.net/*
// @exclude https://www.zerochan.net/pm*
// @exclude https://www.zerochan.net/moe
// @exclude https://www.zerochan.net/faq
// @exclude https://www.zerochan.net/about
// @exclude https://www.zerochan.net/privacy
// @exclude https://www.zerochan.net/api
// @exclude https://www.zerochan.net/upload3
// @exclude https://www.zerochan.net/forum
// @exclude https://www.zerochan.net/options*
// @exclude https://www.zerochan.net/report*
// @match https://anime-pictures.net/posts?page=*
// @match https://anime-pictures.net/posts?favorite_by=*
// @match https://anime-pictures.net/posts?user=*
// @match https://anime-pictures.net/stars?page=*
// @match https://konachan.net/post
// @match https://konachan.net/post?tags=*
// @match https://konachan.net/post?page=*
// @match https://kusowanka.com/
// @match https://kusowanka.com/search/*
// @match https://kusowanka.com/tag*
// @match https://kusowanka.com/parod*
// @match https://kusowanka.com/artist*
// @match https://kusowanka.com/character*
// @match https://kusowanka.com/metadata*
// @match https://tbib.org/index.php?page=post&s=list*
// @match https://tbib.org/index.php?page=favorites&s=view&id=*
// @match https://*.booru.org/index.php?page=post&s=list*
// @match https://*.booru.org/index.php?page=favorites&s=view&id=*
// @match https://safebooru.org/index.php?page=post&s=list*
// @match https://safebooru.org/index.php?page=favorites&s=view&id=*
// @match https://xbooru.com/index.php?page=post&s=list*
// @match https://xbooru.com/index.php?page=favorites&s=view&id=*
// @match https://realbooru.com/index.php?page=post&s=list*
// @match https://realbooru.com/index.php?page=favorites&s=view&id=*
// @match https://derpibooru.org/*
// @exclude https://derpibooru.org/filters
// @exclude https://derpibooru.org/settings*
// @exclude https://derpibooru.org/*/new
// @exclude https://derpibooru.org/pages/*
// @exclude https://derpibooru.org/staff
// @match https://twibooru.org/*
// @exclude https://twibooru.org/filters
// @exclude https://twibooru.org/settings*
// @exclude https://twibooru.org/*/new
// @exclude https://twibooru.org/pages/*
// @exclude https://twibooru.org/staff
// @match https://manebooru.art/*
// @exclude https://manebooru.art/filters
// @exclude https://manebooru.art/settings*
// @exclude https://manebooru.art/*/new
// @exclude https://manebooru.art/pages/*
// @exclude https://manebooru.art/staff
// @match http://browse.minitokyo.net/gallery*
// @match http://gallery.minitokyo.net/*
// @match https://inkbunny.net/submissionsviewall.php*
// @match https://inkbunny.net/usersviewall.php*
// @match https://inkbunny.net/gallery/*
// @match https://hypnohub.net/index.php?page=post&s=list*
// @match https://hypnohub.net/index.php?page=favorites&s=view&id=*
// @match https://booru.eu/post/list*
// @grant none
// @homepageURL https://greasyfork.org/en/scripts/558617-booru-keybinds
// @supportURL https://greasyfork.org/en/scripts/558617-booru-keybinds/feedback
// @license MIT-0
// ==/UserScript==
//
// FEATURES
// - Use left/right arrow keys to navigate back and forward instead of clicking pagination buttons!
// - Minimally supports browse and search page types.
// - Extended page type support is spotty and may include user uploads/favorites and more.
// - Somewhat intelligent behavior!
// - Avoids navigation when user has input fields focused.
// - Avoids inappropriate back navigation on first pages, homepages, etc.
//
//
// CHANGELOG
// v3.2 Various improvements
// - Convert remaining URIs matched by regex to globular
// - Add support for InkBunny user gallery pages
// - Fix browser-native key events being caught and suppressed (e.g. alt+arrow)
// v3.1 Various improvements
// - Auto-detect sites running booru-on-rails and derivatives
// - Move to globular URI matching for more sites
// - Support more pages across already supported sites
// - Add in-code URI evaluation for enhanced exclusion logic e.g. zerochan post pages
// v3.0.1 Bugfixes, and improvements to Minitokyo handling
// v3.0 Add support for booru.eu
// v2.6 Add support for minitokyo.net, inkbunny.net, and hypnohub.net
// v2.4 Add support for derpibooru.org, twibooru.org, and manebooru.art
// v2.3 Add support for tbib.org, safebooru.org, xbooru.com, realbooru.com, and booru.org
// v2.2 Add support for zerochan.net, anime-pictures.net, konachan.net, and kusomanka.com
// v2.1 Add support for rule34.us and gelbooru.com
// v2.0 Add support for yande.re
// v1.0 Initial release with support for rule34.xxx
//
//
// TODO
// - Add 'last page' detection logic to prevent inappropriate forward navigation.
// - Briefly change the cursor to visually indicate when a requested nav event fails or is rejected.
// - Add support for comment list page on rule34.xxx
// https://rule34.xxx/index.php?page=comment&s=user&user=* (pid offset of 15)
// - rule34.us needs special handling for user favorites: ?page=1 doesn't work and must be removed.
// - Alternate keybinds (A/D) for 60% keyboard users (NOTE: gelbooru and booru.org already support these!)
// - Add support for rule34.world, rule34vault.com, rule34.xyz, and furry34.com
// - Add support for Weasyl.com, FurAffinity.net, and Hentai-Foundry.com
//
(function() {
'use strict';
// for domains that use page numbers in an url parameter
// { domain: [ parameter, first page] }
const PAGE_DOMAINS = {
'yande.re': ['page', 1],
'rule34.us': ['page', 1],
'zerochan.net': ['p', 1],
'anime-pictures.net': ['page', 0],
'konachan.net': ['page', 1],
'kusowanka.com': ['page', 1],
'derpibooru.org': ['page', 1],
'twibooru.org': ['page', 1],
'manebooru.art': ['page', 1],
'minitokyo.net': ['page', 1],
'inkbunny.net': ['page', 1]
};
// for domains that put item count in an url parameter
// { domain: items per page }
const PID_DOMAINS = {
'rule34.xxx': 42,
'gelbooru.com': 42,
'tbib.org': 42,
'booru.org': 20,
'safebooru.org': 42,
'xbooru.com': 42,
'realbooru.com': 42,
'hypnohub.net': 42
};
// booru-on-rails and derivatives (philomena)
const BOR_SITES = [
'derpibooru.org',
'twibooru.org',
'manebooru.art'
];
const DEBUG = false;
let DEBUG_TEXT = [];
let PATH = window.location.pathname;
let PARAMS = new URLSearchParams(window.location.search);
const FQDN = window.location.hostname;
let DOMAIN = FQDN;
const domain_parts = FQDN.split('.');
if (domain_parts.length >= 3) {
DOMAIN = (
domain_parts[domain_parts.length - 2]
+ '.' + domain_parts[domain_parts.length - 1]
);
}
function isPathExcluded() {
// in place of using regex for a host match exclusion,
// ^https:\/\/www\.zerochan\.net\/[1-9][0-9]+
if (DOMAIN === 'zerochan.net'
&& PATH.substring(1)
&& !isNaN(parseInt(PATH.substring(1)))
) {
return true;
}
return false;
}
function detectPaginationType() {
if (DOMAIN === 'inkbunny.net' && PATH.startsWith('/gallery/')) {
return 'gallery';
} else if (DOMAIN in PID_DOMAINS) {
return 'pid';
} else if (DOMAIN in PAGE_DOMAINS) {
return 'page';
} else if (PATH.startsWith('/post/list')) {
return 'path';
}
return null;
}
function getCurrentPage(type) {
let page;
if (!type) type = detectPaginationType();
if (type === 'pid') {
// assume parameter is named 'pid' and first item is 0
page = parseInt(
PARAMS.get(
type
)) || 0;
} else if (type === 'page') {
// special case for zerochan frontpage
if (
DOMAIN == 'www.zerochan.net'
&& PATH === '/'
&& !PARAMS.has(PAGE_DOMAINS[DOMAIN][0])
) {
page = 0;
} else {
// retrieve parameter name from configuration
// if no such param was passed, return known first page for domain
page = parseInt(
PARAMS.get(
PAGE_DOMAINS[DOMAIN][0]
)) || PAGE_DOMAINS[DOMAIN][1];
}
} else if (type === 'path') {
let path_parts = PATH.split('/');
if (path_parts.length >= 4) {
page = parseInt(
path_parts[path_parts.length - 1]
) || 1;
} else {
page = 1;
}
} else if (type === 'gallery') {
// for /gallery/USERNAME/PAGE/HASH, PAGE is at position -2
let path_parts = PATH.split('/');
if (path_parts.length >= 4) {
page = parseInt(
path_parts[path_parts.length - 2]
) || 1;
} else {
page = 1;
}
}
return page;
}
function calculatePreviousPage(type, current, pageLength) {
if (type === 'pid') {
if (current > 0) {
return Math.max(0, current - pageLength);
} else {
if (DEBUG) console.log(`(current=${current}) <= 0`);
return false;
}
} else if (type === 'page') {
if (current > PAGE_DOMAINS[DOMAIN][1]) {
return (current - 1);
} else {
if (DEBUG) console.log(`(current=${current}) <= (PAGE_DOMAINS[DOMAIN][1]=${PAGE_DOMAINS[DOMAIN][1]})`);
return false;
}
} else if (type === 'path' || type === 'gallery') {
if (current > 1) {
return (current - 1);
} else {
if (DEBUG) console.log(`(current=${current}) <= 1`);
return false;
}
}
}
function calculateNextPage(type, current, pageLength) {
if (type === 'pid') {
return (current + pageLength);
} else if (type === 'page' || type === 'path' || type === 'gallery') {
return (current + 1);
}
}
function updateQuery(type, dest) {
if (type === 'pid') {
// assume parameter is named 'pid'
PARAMS.set(type, dest);
} else if (type === 'page') {
PARAMS.set(PAGE_DOMAINS[DOMAIN][0], dest);
} else if (type === 'path') {
let path_parts = PATH.split('/');
if (path_parts.length >= 4) path_parts.pop();
path_parts.push(dest.toString());
PATH = path_parts.join('/');
} else if (type === 'gallery') {
// for /gallery/USERNAME/PAGE/HASH, replace PAGE at position -2
let path_parts = PATH.split('/');
if (path_parts.length >= 4) {
path_parts[path_parts.length - 2] = dest.toString();
PATH = path_parts.join('/');
}
}
return true;
}
function reformatUri() {
// redirect frontpage to proper listing page for booru-on-rails sites
const footer = document.querySelector('#serving_info');
if (((typeof BOR_SITES === 'object'
&& BOR_SITES.includes(DOMAIN))
|| (footer !== null
&& footer.tagName === 'DIV'
&& (footer.textContent.includes('booru-on-rails')
|| footer.textContent.includes('philomena'))))
&& PATH === '/'
) {
if (DOMAIN == 'twibooru.org') {
PATH = '/posts';
} else {
PATH = '/images';
}
}
// if there are parameters and we've modified them,
// reconstruct the search string
let params_str = PARAMS.toString();
if (PARAMS.size
// && params_str !== window.location.search
) {
return `${PATH}?${params_str}`;
} else {
return PATH;
}
}
function navigate(direction) {
PARAMS = new URLSearchParams(window.location.search);
const type = detectPaginationType();
let current = null;
current = getCurrentPage(type);
let pageLength = null;
if (type === 'pid') pageLength = PID_DOMAINS[DOMAIN];
let dest = null;
if (direction === 'previous') {
dest = calculatePreviousPage(type, current, pageLength);
} else if (direction === 'next') {
dest = calculateNextPage(type, current, pageLength);
}
if (dest === false || dest === null) return false;
updateQuery(type, dest);
if (DEBUG) {
DEBUG_TEXT.push(
`Keybind caught: direction=${direction}, type=${type}, `
+ `current=${current}, dest=${dest}, pageLength=${pageLength}, `
+ `PARAMS=${PARAMS.toString()}, PATH=${PATH}`
);
}
PATH = reformatUri();
window.location.href = PATH;
}
// activation conditions
if (isPathExcluded()) return false;
const type = detectPaginationType();
if (!type) return false;
document.addEventListener('keydown', function(e) {
// ignore modifiers
if (e.ctrlKey || e.altKey || e.shiftKey || e.metaKey) return;
// ignore keypress when input field is focused
if (
e.target.tagName === 'INPUT'
|| e.target.tagName === 'TEXTAREA'
|| e.target.getAttribute('role') == 'search'
) return;
switch(e.key) {
case 'ArrowLeft':
e.preventDefault();
navigate('previous');
break;
case 'ArrowRight':
e.preventDefault();
navigate('next');
break;
}
});
console.log(`Keybinds loaded (${type} mode). Use ← → arrow keys to navigate gallery pages.`);
console.debug(DEBUG_TEXT.join('\n'));
})();