booru keybinds

Keybinds for navigating boorus

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

You will need to install an extension such as Tampermonkey to install this script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

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