Bunpro Toolbox

Adds search links for Japanese resources

Dovrai installare un'estensione come Tampermonkey, Greasemonkey o Violentmonkey per installare questo script.

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

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Userscripts per installare questo script.

Dovrai installare un'estensione come ad esempio Tampermonkey per installare questo script.

Dovrai installare un gestore di script utente per installare questo script.

(Ho già un gestore di script utente, lasciamelo installare!)

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

(Ho già un gestore di stile utente, lasciamelo installare!)

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