Bunpro Toolbox

Adds search links for Japanese resources

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

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

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name         Bunpro Toolbox
// @version      1.7.0
// @description  Adds search links for Japanese resources
// @author       Ambo100
// @match        *://bunpro.jp/grammar_points/*
// @match        *://bunpro.jp/learn*
// @match        *://bunpro.jp/vocabs/*
// @grant        none
// @namespace    https://greasyfork.org/users/230700
// ==/UserScript==

(function () {
    'use strict';

    const STYLE_ID = 'bp-toolbox-style';
    const POLL_MS = 1000;

    const GRAMMAR_TEMPLATE = `
Core Sections:
- Meaning
- Structure
- Usage
- Example Sentences
- Common Patterns
- Facts

Optional:
- Facts
- Contrast
- Etymology
- Common Mistakes
`;

    const VOCAB_TEMPLATE = `
Core Sections:
- Meaning
- Nuance
- Usage
- Example Sentences

Optional:
- Facts
- Kanji Breakdown
- Common Collocations
- Related Vocab
`;

    const AI_TEMPLATE = `
Role: Japanese Tutor
Task: Explain this Japanese {itemType} clearly, practically, and in a structured textbook style.
Input: {currentItem}
Rules:
- Follow the section order exactly
- Use clear headings, tables and pretty formatting. Textbook style.
- Give 3 natural Japanese example sentences with English translations
- No romaji
- Include upto 3-4 concise facts
- Include common mistakes if relevant

{template}
`;

    const CLASSES = {
        launcher: 'bp-toolbox-launcher',
        panel: 'bp-toolbox-panel',
        open: 'bp-toolbox-open',
        header: 'bp-toolbox-header',
        title: 'bp-toolbox-title',
        body: 'bp-toolbox-body',
        section: 'bp-toolbox-section',
        sectionTitle: 'bp-toolbox-section-title',
        links: 'bp-toolbox-links',
        button: 'bp-toolbox-button',
        close: 'bp-toolbox-close'
    };

    const RESOURCES = [
        {
            title: 'Dictionaries',
            items: [
                { label: 'Jisho', url: 'https://jisho.org/search/{currentItem}' },
                { label: 'Wiktionary', url: 'https://en.wiktionary.org/wiki/{currentItem}#Japanese' },
                { label: 'Eijirou', url: 'https://eow.alc.co.jp/search?q={currentItem}' },
                { label: 'Weblio', url: 'https://www.weblio.jp/content/{currentItem}' }
            ]
        },
        {
            title: 'Video',
            items: [
                { label: 'YouGlish', url: 'https://youglish.com/pronounce/{currentItem}/japanese?' },
                { label: 'YouTube', url: 'https://www.youtube.com/results?search_query={currentItem}+Japanese' }
            ]
        },
        {
            title: 'Community',
            items: [
                { label: 'Stack Exchange', url: 'https://japanese.stackexchange.com/search?q={currentItem}' },
                { label: 'Reddit', url: 'https://www.reddit.com/r/LearnJapanese/search?q={currentItem}&restrict_sr=on&sort=relevance&t=all' },
                { label: 'WK Forum', url: 'https://community.wanikani.com/search?q={currentItem}%20category%3A17' }
            ]
        },
        {
            title: 'AI',
            items: [
                { label: 'ChatGPT', url: 'https://chat.openai.com/?q={prompt}', ai: true },
                { label: 'Claude', url: 'https://claude.ai/new?q={prompt}', ai: true }
            ]
        }
    ];

    const STYLES = `
        .${CLASSES.launcher} {
            position: fixed;
            right: 20px;
            bottom: 20px;
            z-index: 2147483647;
            min-height: 40px;
            padding: 0 14px;
            border: 1px solid rgba(255,255,255,0.16);
            border-radius: 999px;
            background: #1f2933;
            color: #e5e7eb;
            cursor: pointer;
            font: 600 14px system-ui, sans-serif;
        }

        .${CLASSES.panel} {
            position: fixed;
            right: 20px;
            bottom: 68px;
            z-index: 2147483647;
            width: 300px;
            max-height: 70vh;
            display: flex;
            flex-direction: column;
            border: 1px solid rgba(255,255,255,0.12);
            border-radius: 12px;
            background: #1f2933;
            color: #e5e7eb;
            box-shadow: 0 12px 30px rgba(0,0,0,0.3);
            overflow: hidden;
            opacity: 0;
            pointer-events: none;
            transform: translateY(8px);
            transition: opacity 120ms ease, transform 120ms ease;
        }

        .${CLASSES.panel}.${CLASSES.open} {
            opacity: 1;
            pointer-events: auto;
            transform: translateY(0);
        }

        .${CLASSES.header} {
            display: flex;
            align-items: center;
            justify-content: space-between;
            gap: 8px;
            padding: 12px;
            border-bottom: 1px solid rgba(255,255,255,0.08);
        }

        .${CLASSES.title} {
            min-width: 0;
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
            font: 600 14px system-ui, sans-serif;
        }

        .${CLASSES.close} {
            width: 28px;
            height: 28px;
            padding: 0;
        }

        .${CLASSES.body} {
            overflow-y: auto;
            padding: 12px;
            display: flex;
            flex-direction: column;
            gap: 12px;
        }

        .${CLASSES.section} {
            display: flex;
            flex-direction: column;
            gap: 8px;
        }

        .${CLASSES.sectionTitle} {
            font: 600 11px system-ui, sans-serif;
            letter-spacing: 0.04em;
            text-transform: uppercase;
            opacity: 0.7;
        }

        .${CLASSES.links} {
            display: flex;
            flex-wrap: wrap;
            gap: 8px;
        }

        .${CLASSES.button} {
            display: inline-flex;
            align-items: center;
            justify-content: center;
            min-height: 32px;
            padding: 0.45rem 0.7rem;
            border: 1px solid rgba(255,255,255,0.14);
            border-radius: 999px;
            background: transparent;
            color: inherit;
            text-decoration: none;
            cursor: pointer;
            font: 500 13px system-ui, sans-serif;
            line-height: 1.2;
            transition: transform 120ms ease, background-color 120ms ease, border-color 120ms ease;
        }

        .${CLASSES.button}:hover,
        .${CLASSES.button}:focus-visible {
            background: rgba(255,255,255,0.08);
            border-color: rgba(255,255,255,0.3);
            transform: translateY(-1px);
            outline: none;
        }
    `;

    let currentUrl = window.location.href;
    let currentItem = '';
    let launcher = null;
    let panel = null;
    let titleEl = null;
    let bodyEl = null;

    window.addEventListener('load', initialize, false);

    function resolveCurrentItem() {
        const itemNode = document.querySelector('h1 .bp-ddw, .bp-ddw');
        if (!itemNode) return '';

        const clonedNode = itemNode.cloneNode(true);
        clonedNode.querySelectorAll('rt, rp').forEach((rubyElement) => rubyElement.remove());

        return clonedNode.textContent?.trim() || '';
    }

    function getPageType() {
        const path = window.location.pathname;
        if (path.startsWith('/vocabs/')) return 'vocab';
        if (path.startsWith('/grammar_points/')) return 'grammar';
        return '';
    }

    function getItemType() {
        return getPageType() === 'vocab' ? 'vocabulary item' : 'grammar point';
    }

    function getSubTemplate() {
        return getPageType() === 'vocab'
            ? VOCAB_TEMPLATE.trim()
            : GRAMMAR_TEMPLATE.trim();
    }

    function getAiPrompt() {
        return AI_TEMPLATE
            .replace('{itemType}', getItemType())
            .replace('{currentItem}', currentItem)
            .replace('{template}', getSubTemplate())
            .trim();
    }

    function initialize() {
        ensureStyles();

        const nextItem = resolveCurrentItem();
        if (!nextItem) {
            currentItem = '';
            removeUI();
            return;
        }

        ensureUI();

        const needsRender =
            nextItem !== currentItem ||
            !bodyEl ||
            !bodyEl.childElementCount;

        currentItem = nextItem;
        updateTitle();

        if (needsRender) {
            renderSections();
        }
    }

    function ensureStyles() {
        if (document.getElementById(STYLE_ID)) return;

        const style = document.createElement('style');
        style.id = STYLE_ID;
        style.textContent = STYLES;
        document.head.appendChild(style);
    }

    function ensureUI() {
        if (!launcher) {
            launcher = el('button', CLASSES.launcher, 'Toolbox');
            launcher.type = 'button';
            launcher.setAttribute('aria-expanded', 'false');
            launcher.addEventListener('click', togglePanel);
            document.body.appendChild(launcher);
        }

        if (!panel) {
            panel = el('section', CLASSES.panel);

            const header = el('div', CLASSES.header);
            titleEl = el('div', CLASSES.title);

            const close = el('button', `${CLASSES.button} ${CLASSES.close}`, '×');
            close.type = 'button';
            close.addEventListener('click', closePanel);

            bodyEl = el('div', CLASSES.body);

            header.append(titleEl, close);
            panel.append(header, bodyEl);
            document.body.appendChild(panel);
        }

        bindGlobalEvents();
    }

    function bindGlobalEvents() {
        if (document.body.dataset.bpToolboxBound === 'true') return;
        document.body.dataset.bpToolboxBound = 'true';

        document.addEventListener('keydown', (event) => {
            if (event.key === 'Escape') closePanel();
        });

        document.addEventListener('click', (event) => {
            if (!panel?.classList.contains(CLASSES.open)) return;
            if (panel?.contains(event.target) || launcher?.contains(event.target)) return;
            closePanel();
        });
    }

    function renderSections() {
        if (!bodyEl) return;

        bodyEl.innerHTML = '';

        for (const group of RESOURCES) {
            const links = group.items.map(buildItem).filter(Boolean);
            if (!links.length) continue;

            const section = el('div', CLASSES.section);
            const heading = el('div', CLASSES.sectionTitle, group.title);
            const linkWrap = el('div', CLASSES.links);

            linkWrap.append(...links);
            section.append(heading, linkWrap);
            bodyEl.appendChild(section);
        }
    }

    function buildItem(item) {
        const url = buildUrl(item);
        if (!url) return null;

        const link = el('a', CLASSES.button, item.label);
        link.href = url;
        link.target = '_blank';
        link.rel = 'noopener noreferrer';

        return link;
    }

    function buildUrl(item) {
        if (!currentItem || !item.url) return '';

        if (item.ai) {
            const prompt = getAiPrompt();
            return item.url.replace('{prompt}', encodeURIComponent(prompt));
        }

        if (item.prompt) {
            const prompt = item.prompt.replace('{currentItem}', currentItem);
            return item.url.replace('{prompt}', encodeURIComponent(prompt));
        }

        return item.url.replace('{currentItem}', encodeURIComponent(currentItem));
    }

    function updateTitle() {
        if (titleEl) {
            titleEl.textContent = currentItem || 'Bunpro Toolbox';
        }
    }

    function togglePanel() {
        panel?.classList.contains(CLASSES.open) ? closePanel() : openPanel();
    }

    function openPanel() {
        if (!panel) return;
        panel.classList.add(CLASSES.open);
        launcher?.setAttribute('aria-expanded', 'true');
    }

    function closePanel() {
        if (!panel) return;
        panel.classList.remove(CLASSES.open);
        launcher?.setAttribute('aria-expanded', 'false');
    }

    function removeUI() {
        launcher?.remove();
        panel?.remove();
        launcher = null;
        panel = null;
        titleEl = null;
        bodyEl = null;
    }

    function el(tag, className = '', text = '') {
        const node = document.createElement(tag);
        if (className) node.className = className;
        if (text) node.textContent = text;
        return node;
    }

    setInterval(() => {
        if (window.location.href !== currentUrl) {
            currentUrl = window.location.href;
            initialize();
        }
    }, POLL_MS);
})();