Bunpro Toolbox

Adds search links for Japanese resources

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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