Bunpro Toolbox

Adds search links for Japanese resources

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

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

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

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