booru keybinds

Keybinds for navigating boorus

2025-12-18 기준 버전입니다. 최신 버전을 확인하세요.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

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

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name            booru keybinds
// @namespace       861ddd094884eac5bea7a3b12e074f34
// @version         3.4
// @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*
// @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
// @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 A/D or 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.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
// - Additional site support:
//   - Weasyl.com, FurAffinity.net, and Hentai-Foundry.com
//   - rule34.us needs special handling for user favorites: ?page=1 doesn't work and must be removed.
//   - rule34.xxx comment list page
//     https://rule34.xxx/index.php?page=comment&s=user&user=* (pid offset of 15)
// - 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.
//

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

    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 ((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
        let params_str = PARAMS.toString();
        if (PARAMS.size
            // && params_str !== window.location.search
        ) {
            return `${PATH}?${params_str}`;
        } else {
            return PATH;
        }
    }

    function navigate(direction) {
        PATH = window.location.pathname;
        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;
    }

    // function isKeybindExcluded(key) {
    //     if ((DOMAIN == 'gelbooru.com' || DOMAIN == 'booru.org')
    //         && (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');
        }
    });

    console.log(`Keybinds loaded (${type} mode). Use ← → arrow keys to navigate gallery pages.`);
    console.debug(DEBUG_TEXT.join('\n'));
})();