Greasy Fork

Greasy Fork is available in English.

猴子都会用的Bangumi bbcode辅助工具

在 Bangumi 文本框工具栏中添加对齐按钮、渐变生成器和图片尺寸调整功能

目前为 2024-11-07 提交的版本,查看 最新版本

// ==UserScript==
// @name         猴子都会用的Bangumi bbcode辅助工具
// @namespace    https://github.com/wakabayu
// @version      1.4
// @description  在 Bangumi 文本框工具栏中添加对齐按钮、渐变生成器和图片尺寸调整功能
// @include      /^https?:\/\/(bgm\.tv|chii\.in|bangumi\.tv)\/.*/
// @grant        none
// @author      wataame
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    let previewWindow = null;
    let lastSelectedText = '';

    // 添加对齐和渐变按钮
    function addToolbarButtons() {
        document.querySelectorAll('.markItUpHeader').forEach(toolbar => {
            if (toolbar.querySelector('.alignmentButton') || toolbar.querySelector('.gradientButton')) return;

            const textarea = toolbar.closest('.markItUpContainer').querySelector('textarea');
            if (!textarea) return;

            const alignments = [
                { label: '◧L', bbcode: 'left', title: '左对齐 [left]' },
                { label: '▣C', bbcode: 'center', title: '居中对齐 [center]' },
                { label: '◨R', bbcode: 'right', title: '右对齐 [right]' }
            ];
            alignments.forEach(alignment => addButton(toolbar, alignment.label, alignment.title, () => applyBBCode(textarea, alignment.bbcode)));

            addButton(toolbar, '渐变', '生成渐变文字', () => {
                const selectedText = getSelectedText(textarea);
                if (!selectedText) return alert('请先选中需要应用渐变的文字');
                openColorPicker(selectedText, textarea);
            });
        });
    }

    function addButton(toolbar, label, title, onClick) {
        const button = document.createElement('a');
        button.href = 'javascript:void(0);';
        button.className = `${title.includes('对齐') ? 'alignmentButton' : 'gradientButton'}`;
        button.title = title;
        button.innerHTML = `<span style="font-weight: bold; margin: 0 8px;">${label}</span>`;
        button.onclick = onClick;
        button.style.margin = '0 6px';
        toolbar.appendChild(button);
    }

    function applyBBCode(textarea, bbcode) {
        const selectedText = getSelectedText(textarea);
        const wrappedText = `[${bbcode}]${selectedText}[/${bbcode}]`;
        replaceSelectedText(textarea, wrappedText);
    }

    function getSelectedText(textarea) {
        return textarea.value.substring(textarea.selectionStart, textarea.selectionEnd);
    }

    function replaceSelectedText(textarea, newText) {
        const start = textarea.selectionStart;
        const end = textarea.selectionEnd;
        textarea.setRangeText(newText, start, end, 'end');
    }

    function openColorPicker(selectedText, textarea) {
        if (document.getElementById('colorPickerContainer')) return;

        const colorPickerContainer = createPopupContainer();
        colorPickerContainer.innerHTML = `
            <label>选择起始颜色:<input type="color" value="#0ebeff" id="startColor"></label><br>
            <label>选择结束颜色:<input type="color" value="#5e95e6" id="endColor"></label><br>
            <label>输入步数:<input type="number" min="1" value="${selectedText.length}" id="steps"></label><br>
            <button id="generate">生成</button> <button id="cancel">取消</button><br>
            <div id="historyContainer" style="margin-top: 10px;"><strong>最近使用的方案:</strong><br></div>
        `;

        document.body.appendChild(colorPickerContainer);
        loadGradientHistory();

        document.querySelector('#generate').onclick = () => {
            const startColor = document.querySelector('#startColor').value;
            const endColor = document.querySelector('#endColor').value;
            const steps = parseInt(document.querySelector('#steps').value);
            if (isNaN(steps) || steps <= 0) return alert('请输入有效的步数');

            const gradientText = generateGradientText(selectedText, startColor, endColor, steps);
            replaceSelectedText(textarea, gradientText);

            saveGradientHistory(startColor, endColor);
            closePopup(colorPickerContainer);
        };

        document.querySelector('#cancel').onclick = () => closePopup(colorPickerContainer);
    }

    function generateGradientText(text, startColor, endColor, steps) {
        const startRGB = hexToRgb(startColor), endRGB = hexToRgb(endColor);
        const segmentLength = Math.ceil(text.length / steps);
        return Array.from({ length: steps }, (_, i) => {
            const ratio = i / (steps - 1);
            const color = `#${rgbToHex(interpolate(startRGB.r, endRGB.r, ratio))}${rgbToHex(interpolate(startRGB.g, endRGB.g, ratio))}${rgbToHex(interpolate(startRGB.b, endRGB.b, ratio))}`;
            return `[color=${color}]${text.slice(i * segmentLength, (i + 1) * segmentLength)}[/color]`;
        }).join('');
    }

    function interpolate(start, end, ratio) {
        return clamp(Math.round(start + ratio * (end - start)));
    }

    const clamp = value => Math.max(0, Math.min(255, value));
    const hexToRgb = hex => ({ r: parseInt(hex.slice(1, 3), 16), g: parseInt(hex.slice(3, 5), 16), b: parseInt(hex.slice(5, 7), 16) });
    const rgbToHex = value => value.toString(16).padStart(2, '0');

    function createPopupContainer() {
        const container = document.createElement('div');
        container.id = 'colorPickerContainer';
        container.style = "position:fixed;top:50%;left:50%;transform:translate(-50%, -50%);background:#fff;padding:20px;border:1px solid #ccc;z-index:9999;box-shadow:0 0 10px rgba(0,0,0,0.1)";
        return container;
    }

    function closePopup(container) {
        document.body.removeChild(container);
    }

    function saveGradientHistory(startColor, endColor) {
        const history = JSON.parse(localStorage.getItem('gradientHistory') || '[]');
        const newEntry = { start: startColor, end: endColor };
        if (!history.some(entry => entry.start === startColor && entry.end === endColor)) {
            history.unshift(newEntry);
            if (history.length > 5) history.pop();
            localStorage.setItem('gradientHistory', JSON.stringify(history));
        }
    }

    function loadGradientHistory() {
        const historyContainer = document.querySelector('#historyContainer');
        const history = JSON.parse(localStorage.getItem('gradientHistory') || '[]');
        historyContainer.innerHTML = '<strong>最近方案:</strong><br>';
        history.forEach((entry, index) => {
            const historyButton = document.createElement('button');
            historyButton.style = `background: linear-gradient(to right, ${entry.start}, ${entry.end}); border: none; color: #fff; padding: 5px; margin: 2px; cursor: pointer;`;
            historyButton.innerText = `方案 ${index + 1}`;
            historyButton.onclick = () => {
                document.querySelector('#startColor').value = entry.start;
                document.querySelector('#endColor').value = entry.end;
            };
            historyContainer.appendChild(historyButton);
        });
    }

    function createOrUpdatePreviewWindow(selectedText, textarea) {
        if (previewWindow) previewWindow.remove();

        previewWindow = document.createElement('div');
        previewWindow.id = 'imgPreviewWindow';
        previewWindow.style = `
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background: #fff;
            border: 1px solid #ccc;
            padding: 20px;
            z-index: 9999;
            display: none;
            box-shadow: 0px 0px 10px rgba(0,0,0,0.5);
            max-width: 90vw;
            max-height: 90vh;
            overflow: auto; /* 确保在预览窗口内出现滚动条 */
        `;

        const imgContainer = document.createElement('div');
        imgContainer.style = 'text-align: center; margin-bottom: 10px;';
        const imgElement = document.createElement('img');
        imgElement.style = 'max-width: 100%; max-height: 80vh; object-fit: contain;'; // 限制图片最大显示高度
        imgContainer.appendChild(imgElement);

        const sliderContainer = document.createElement('div');
        sliderContainer.style = 'display: flex; align-items: center; margin-top: 10px;';

        const sliderLabel = document.createElement('span');
        sliderLabel.innerText = '调整尺寸: ';

        const slider = document.createElement('input');
        slider.type = 'range';
        slider.min = 10;
        slider.max = 100;
        slider.value = 100;
        slider.style = 'margin-left: 5px; flex-grow: 1;';

        const applyButton = document.createElement('button');
        applyButton.innerText = '应用';
        applyButton.style = 'margin-top: 10px;';

        const closeButton = document.createElement('button');
        closeButton.innerText = '关闭';
        closeButton.style = 'margin-top: 10px; margin-left: 10px;';

        sliderContainer.appendChild(sliderLabel);
        sliderContainer.appendChild(slider);
        previewWindow.appendChild(imgContainer);
        previewWindow.appendChild(sliderContainer);
        previewWindow.appendChild(applyButton);
        previewWindow.appendChild(closeButton);
        document.body.appendChild(previewWindow);

        closeButton.onclick = () => {
            previewWindow.style.display = 'none';
            lastSelectedText = '';
        };

        applyButton.onclick = () => {
            const naturalWidth = imgElement.naturalWidth;
            const naturalHeight = imgElement.naturalHeight;
            const scale = slider.value / 100;
            const scaledWidth = Math.round(naturalWidth * scale);
            const scaledHeight = Math.round(naturalHeight * scale);
            const newCode = `[img=${scaledWidth},${scaledHeight}]${imgElement.src}[/img]`;
            textarea.value = textarea.value.replace(selectedText, newCode);
            previewWindow.style.display = 'none';
            lastSelectedText = '';
        };

        slider.oninput = () => {
            const naturalWidth = imgElement.naturalWidth;
            const naturalHeight = imgElement.naturalHeight;
            const scale = slider.value / 100;
            imgElement.width = naturalWidth * scale;
            imgElement.height = naturalHeight * scale;
            adjustPreviewWindowSize(imgElement.width, imgElement.height);
        };

        const imgTagRegex = /\[img(?:=(\d+),(\d+))?\](https?:\/\/[^\s]+)\[\/img\]/;
        const match = selectedText.match(imgTagRegex);

        if (!match) return;

        const initialWidth = match[1] ? parseInt(match[1], 10) : null;
        const initialHeight = match[2] ? parseInt(match[2], 10) : null;
        const imgURL = match[3];
        imgElement.src = imgURL;

        imgElement.onload = () => {
            const naturalWidth = imgElement.naturalWidth;
            const naturalHeight = imgElement.naturalHeight;

            if (initialWidth && initialHeight) {
                const widthRatio = initialWidth / naturalWidth;
                const heightRatio = initialHeight / naturalHeight;
                const scale = Math.min(widthRatio, heightRatio) * 100;
                slider.value = Math.max(10, Math.min(scale, 100));

                imgElement.width = naturalWidth * (slider.value / 100);
                imgElement.height = naturalHeight * (slider.value / 100);
            } else {
                imgElement.width = naturalWidth;
                imgElement.height = naturalHeight;
            }

            adjustPreviewWindowSize(imgElement.width, imgElement.height);
        };

        previewWindow.style.display = 'block';
    }

    function adjustPreviewWindowSize(width, height) {
        const maxWidth = window.innerWidth * 0.9;
        const maxHeight = window.innerHeight * 0.9;
        previewWindow.style.width = `${Math.min(width + 40, maxWidth)}px`;
        previewWindow.style.height = `${Math.min(height + 80, maxHeight)}px`;
    }

    function handleSelectionChange() {
        const textarea = document.activeElement;
        if (textarea && textarea.tagName === 'TEXTAREA') {
            const selectedText = textarea.value.substring(textarea.selectionStart, textarea.selectionEnd).trim();
            if (selectedText !== lastSelectedText && selectedText.match(/\[img(?:=(\d+),(\d+))?\]https?:\/\/[^\s]+\[\/img\]/)) {
                createOrUpdatePreviewWindow(selectedText, textarea);
                lastSelectedText = selectedText;
            }
        }
    }

    const observer = new MutationObserver(() => addToolbarButtons());
    observer.observe(document.body, { childList: true, subtree: true });
    document.addEventListener('selectionchange', handleSelectionChange);
})();