Keybinds for navigating boorus
Ajankohdalta
// ==UserScript==
// @name booru keybinds
// @namespace 861ddd094884eac5bea7a3b12e074f34
// @version 3.5.1
// @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://inkbunny.net/scraps/*
// @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*
// @match https://rule34vault.com/*
// @exclude https://rule34vault.com/trends*
// @exclude https://rule34vault.com/comments*
// @exclude https://rule34vault.com/u/*?tab=subs
// @exclude https://rule34vault.com/u/*?tab=comments
// @exclude https://rule34vault.com/account*
// @exclude https://rule34vault.com/post/*
// @exclude https://rule34vault.com/dmca
// @exclude https://rule34vault.com/terms
// @exclude https://rule34vault.com/contact-us
// @exclude https://rule34vault.com/upgrade-to-premium
// @match https://rule34.xyz/*
// @exclude https://rule34.xyz/trends*
// @exclude https://rule34.xyz/comments*
// @exclude https://rule34.xyz/u/*?tab=subs
// @exclude https://rule34.xyz/u/*?tab=comments
// @exclude https://rule34.xyz/account*
// @exclude https://rule34.xyz/post/*
// @exclude https://rule34.xyz/dmca
// @exclude https://rule34.xyz/terms
// @exclude https://rule34.xyz/contact-us
// @exclude https://rule34.xyz/upgrade-to-premium
// @match https://rule34.world/*
// @exclude https://rule34.world/trends*
// @exclude https://rule34.world/comments*
// @exclude https://rule34.world/u/*?tab=subs
// @exclude https://rule34.world/u/*?tab=comments
// @exclude https://rule34.world/account*
// @exclude https://rule34.world/post/*
// @exclude https://rule34.world/dmca
// @exclude https://rule34.world/terms
// @exclude https://rule34.world/contact-us
// @exclude https://rule34.world/upgrade-to-premium
// @match https://furry34.com/*
// @exclude https://furry34.com/trends*
// @exclude https://furry34.com/comments*
// @exclude https://furry34.com/u/*?tab=subs
// @exclude https://furry34.com/u/*?tab=comments
// @exclude https://furry34.com/account*
// @exclude https://furry34.com/post/*
// @exclude https://furry34.com/dmca
// @exclude https://furry34.com/terms
// @exclude https://furry34.com/contact-us
// @exclude https://furry34.com/upgrade-to-premium
// @match https://www.hentai-foundry.com/categories/*/pictures*
// @match https://www.hentai-foundry.com/pictures/recent*
// @match https://www.hentai-foundry.com/pictures/featured*
// @match https://www.hentai-foundry.com/pictures/popular*
// @match https://www.hentai-foundry.com/pictures/random*
// @match https://www.hentai-foundry.com/pictures/user/*
// @match https://www.hentai-foundry.com/pictures/tagged/*
// @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
// - Keyboard shortcuts:
// - Use A/D or left/right arrow keys to navigate through pages instead of clicking buttons!
// - Use W/S or up/down arrow keys for navigating through history.
// - Page types supported:
// - Minimally works with browse and search page types across all supported sites.
// - Extended page type support is spotty and may include user uploads/favorites and more.
//
//
// CHANGELOG
// v3.5.1 Enhancements
// - Support InkBunny pages for user scraps
// - Support W/S or up/down arrow keys for navigating through history
// v3.5 Support new sites and fix bugs
// - Add support for hentai-foundry.com
// - Fix zerochan navigation from frontpage
// v3.4.1 Various bugfixes
// - Fix rule34vault.com and furry34.com support
// - Fix rule34.us user favorites page nagivation
// - Fix navigation for anime-pictures.net
// - Optimize code with shorthand
// v3.4 Add A/D navigational keybind alternatives for 60% keyboard users
// v3.3 Add support for rule34vault.com, rule34.xyz, rule34.world, furry34.com
// 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
// - Functional enhancements
// - Support weasyl.com and FurAffinity.net
// - Support rule34.xxx comment list page
// https://rule34.xxx/index.php?page=comment&s=user&user=* (pid offset of 15)
// - Add up arrow and W keybinds for navigating 'back' through history to search results when on post detail page.
// - Nice-to-haves
// - 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.
// - Refinement and refactoring
// - Rewrite 'gallery' page type into abstraction supporting multiple page route exceptions similar to PAGE_PATHS,
// but for path index instead of page minimum.
// - zerochan and Hentai-Foundry post pages ought to be excluded
// https://www.zerochan.net/[1-9][0-9]+
// https://www.hentai-foundry.com/pictures/user/USERNAME/POST_NUMBER/POST_TITLE
//
(function() {
'use strict';
const DEBUG = false;
// 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],
'rule34vault.com': ['page', 1],
'rule34.xyz': ['page', 1],
'rule34.world': ['page', 1],
'furry34.com': ['page', 1],
};
// path exceptions to the site minimums above
const PAGE_PATHS = {
'rule34.us': [
{ 'r': 'favorites/view' }, 0
]
}
// 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
};
// for domains that put page count in a route
// { domain: minimum page number, indexed page position }
const ROUTE_DOMAINS = {
'booru.eu': [1, -1],
'hentai-foundry.com': [1, -1],
}
// booru-on-rails and derivatives (philomena)
const BOR_SITES = ['derpibooru.org', 'twibooru.org', 'manebooru.art'];
// rule34.world operations
const R34WORLD_SITES = ['rule34vault.com', 'rule34.xyz', 'rule34.world'];
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]
);
}
// .startsWith() but accepting an array of strings to check
function startsWithMany(str, arr) {
return arr.some(s => str.startsWith(s));
}
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 getAlternateMinimumPage() {
// Check if current domain/path matches PAGE_PATHS config items
if (DOMAIN in PAGE_PATHS) {
const pathMatch = PAGE_PATHS[DOMAIN][0];
const alternateMin = PAGE_PATHS[DOMAIN][1];
for (const [key, value] of Object.entries(pathMatch)) {
if (!PARAMS.has(key) || PARAMS.get(key) !== value) {
return null;
}
}
return alternateMin;
}
return null;
}
function detectPaginationType() {
if (DOMAIN === 'inkbunny.net'
&& startsWithMany(PATH, ['/gallery/', '/scraps/'])
) {
return 'gallery';
} else if (DOMAIN in PID_DOMAINS) {
return 'pid';
} else if (DOMAIN in PAGE_DOMAINS) {
return 'page';
} else if (DOMAIN in ROUTE_DOMAINS) {
return 'route';
}
return null;
}
function getCurrentPage(type) {
if (!type) type = detectPaginationType();
if (type === 'pid') {
// assume parameter is named 'pid' and first item is 0
return parseInt(
PARAMS.get(
type
)) || 0;
} else if (type === 'page') {
// retrieve parameter name from configuration
// if no such param was passed, return known first page for domain
const alternateMin = getAlternateMinimumPage();
const defaultMin = (alternateMin !== null)
? alternateMin : PAGE_DOMAINS[DOMAIN][1];
return parseInt(
PARAMS.get(PAGE_DOMAINS[DOMAIN][0])
) || defaultMin;
} else if (type === 'route') {
let pathParts = PATH.split('/');
const defaultMin = ROUTE_DOMAINS[DOMAIN][0];
const pathOffset = ROUTE_DOMAINS[DOMAIN][1];
let pathIndex = (pathOffset < 0)
? pathParts.length + pathOffset
: pathOffset;
const current = parseInt(pathParts[pathIndex]);
if (isNaN(current)) {
return defaultMin;
} else {
return current;
}
} else if (type === 'gallery') {
// special snowflake inkbunny puts its page number at position -2
// e.x. /pageName/USERNAME/PAGE/HASH
let pathParts = PATH.split('/');
return parseInt(
pathParts[pathParts.length - 2]
) || PAGE_DOMAINS[DOMAIN];
}
}
function calculatePreviousPage(type, current, pageLength) {
if (type === 'pid') {
if (current > 0) {
return Math.max(0, current - pageLength);
} else {
if (DEBUG) console.debug(`(current=${current}) <= 0`);
return false;
}
} else if (type === 'page') {
const alternateMin = getAlternateMinimumPage();
const minPage = (alternateMin !== null)
? alternateMin : PAGE_DOMAINS[DOMAIN][1];
if (current > minPage) {
return (current - 1);
} else {
if (DEBUG) console.debug(`(current=${current}) <= (minPage=${minPage})`);
return false;
}
} else if (['route', 'gallery'].includes(type)) {
if (current > 1) {
return (current - 1);
} else {
if (DEBUG) console.debug(`(current=${current}) <= 1`);
return false;
}
}
}
function calculateNextPage(type, current, pageLength) {
if (type === 'pid') {
return (current + pageLength);
} else if (['page', 'route', 'gallery'].includes(type)) {
return (current + 1);
}
}
function updateQuery(type, dest) {
const pageNo = dest.toString();
const pageKey = PAGE_DOMAINS[DOMAIN][0];
if (type === 'pid') {
// assume parameter is named 'pid'
PARAMS.set(type, pageNo);
} else if (type === 'page') {
PARAMS.set(pageKey, pageNo);
} else if (type === 'route') {
let pathParts = PATH.split('/');
const pathOffset = ROUTE_DOMAINS[DOMAIN][1];
let pathIndex = (pathOffset < 0)
? pathParts.length + pathOffset
: pathOffset;
if (!isNaN(parseInt(pathParts[pathIndex]))) {
pathParts[pathIndex] = pageNo;
} else {
if (DOMAIN === 'hentai-foundry.com') pathParts.push('page');
pathParts.push(pageNo);
}
PATH = pathParts.join('/');
} else if (type === 'gallery') {
// for /pageName/USERNAME/PAGE/HASH, replace PAGE at position -2
let pathParts = PATH.split('/');
pathParts[pathParts.length - 2] = pageNo;
PATH = pathParts.join('/');
}
return true;
}
function reformatUri() {
// redirect booru-on-rails frontpage to listing page on navigation
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 === '/'
) PATH = (DOMAIN == 'twibooru.org') ? '/posts' : '/images';
// remove unsupported query string parameters
// that break navigation when we alter the page param
if ((typeof R34WORLD_SITES === 'object'
&& R34WORLD_SITES.includes(DOMAIN))
&& PARAMS.has('cursor')
) PARAMS.delete('cursor');
// if there are parameters and we've modified them,
// reconstruct the search string
return (PARAMS.size)
? `${PATH}?${PARAMS.toString()}`
: PATH;
}
function navigate(direction) {
PATH = window.location.pathname;
PARAMS = new URLSearchParams(window.location.search);
const type = detectPaginationType();
let current = null;
current = getCurrentPage(type);
if (DEBUG) console.log(`currentPage = ${current}`);
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);
}
// test specifically for false (0 is a valid page number!!)
if (dest !== 0 && !dest) 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;
}
// function isKeybindExcluded(key) {
// if ((['gelbooru.com', 'booru.org'].includes(DOMAIN))
// && (key == 'a' || key == 'd')
// ) {
// return true;
// } else {
// return false;
// }
// }
// 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;
// ignore keys on sites that partially implement our bindings
// NOTE: native navigation via these keybinds is broken XD
// if (isKeybindExcluded(e.key.toLowerCase())) return;
const key = (e.key || '').toLowerCase();
if (key === 'arrowleft' || key === 'a') {
e.preventDefault();
navigate('previous');
} else if (key === 'arrowright' || key === 'd') {
e.preventDefault();
navigate('next');
} else if (key === 'arrowup' || key === 'w') {
e.preventDefault()
history.back();
} else if (key === 'arrowdown' || key === 's') {
e.preventDefault()
history.forward();
}
});
console.log(`Keybinds loaded (${type} mode). Use ← → arrow keys to navigate gallery pages.`);
if (DEBUG && DEBUG_TEXT) console.debug(DEBUG_TEXT.join('\n'));
})();