Greasy Fork

Greasy Fork is available in English.

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

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

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         猴子都会用的Bangumi bbcode辅助工具
// @namespace    https://github.com/wakabayu
// @version      1.5
// @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 debounce(func, wait) {
        let timeout;
        return function executedFunction(...args) {
            const later = () => {
                clearTimeout(timeout);
                func(...args);
            };
            clearTimeout(timeout);
            timeout = setTimeout(later, wait);
        };
    }

    // 安全执行函数
    function safelyExecute(func, errorMessage) {
        try {
            return func();
        } catch (error) {
            console.error(error);
            alert(errorMessage || '操作失败,请重试');
            return null;
        }
    }

    // 添加对齐和渐变按钮
    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 createPopup(id, content) {
        const popup = document.createElement('div');
        popup.id = id;
        popup.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)
        `;
        popup.innerHTML = content;
        return popup;
    }

    // 颜色选择器相关函数
    function openColorPicker(selectedText, textarea) {
        if (document.getElementById('colorPickerContainer')) return;
        const colorPickerContainer = createPopup('colorPickerContainer', `
            <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 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 = createPopup('imgSizeSelector', `
            <div style="text-align: center; margin-bottom: 15px;">
                <strong>调整图片尺寸</strong>
            </div>
            <div id="previewArea" style="
                margin: 10px auto;
                text-align: center;
                max-height: 300px;
                overflow: auto;
                border: 1px solid #ddd;
                padding: 10px;
            ">
                <img id="previewImage" style="max-width: 100%; height: auto;" />
            </div>
            <div id="sizeInfo" style="margin: 10px 0; text-align: center; font-size: 14px;"></div>
            <div style="display: flex; justify-content: center; gap: 10px; margin: 15px 0;">
                <div id="presetButtons" style="display: flex; gap: 5px;"></div>
            </div>
            <div style="display: flex; justify-content: center; gap: 10px; margin: 15px 0;">
                <input type="number" id="customWidth" placeholder="宽度" style="width: 80px; padding: 5px;">
                <span style="line-height: 30px;">×</span>
                <input type="number" id="customHeight" placeholder="高度" style="width: 80px; padding: 5px;">
                <button id="lockRatio" style="padding: 0 8px;" title="锁定宽高比">🔒</button>
            </div>
            <div style="text-align: center; margin-top: 15px;">
                <button id="applySize" style="margin-right: 10px; padding: 5px 15px;">应用</button>
                <button id="closeWindow" style="padding: 5px 15px;">关闭</button>
            </div>
        `);
        document.body.appendChild(previewWindow);

        // 获取元素引用
        const sizeInfo = previewWindow.querySelector('#sizeInfo');
        const presetButtons = previewWindow.querySelector('#presetButtons');
        const customWidth = previewWindow.querySelector('#customWidth');
        const customHeight = previewWindow.querySelector('#customHeight');
        const lockRatioBtn = previewWindow.querySelector('#lockRatio');
        const applyButton = previewWindow.querySelector('#applySize');
        const closeButton = previewWindow.querySelector('#closeWindow');
        const previewImage = previewWindow.querySelector('#previewImage');
        const previewArea = previewWindow.querySelector('#previewArea');

        // 状态变量
        let originalWidth = 0;
        let originalHeight = 0;
        let selectedWidth = 0;
        let selectedHeight = 0;
        let isRatioLocked = true;
        let aspectRatio = 1;

        // 预设尺寸按钮
        const presets = [
            { label: '25%', value: 0.25 },
            { label: '50%', value: 0.5 },
            { label: '75%', value: 0.75 },
            { label: '100%', value: 1 }
        ];

        // 创建预设按钮
        presets.forEach(preset => {
            const button = document.createElement('button');
            button.innerText = preset.label;
            button.style = `
                padding: 5px 10px;
                cursor: pointer;
                border: 1px solid #ccc;
                border-radius: 4px;
                background: #f0f0f0;
            `;
            button.onclick = () => {
                selectedWidth = Math.round(originalWidth * preset.value);
                selectedHeight = Math.round(originalHeight * preset.value);
                updateInputs();
                highlightButton(button);
            };
            presetButtons.appendChild(button);
        });

        // 锁定比例按钮
        lockRatioBtn.style.background = isRatioLocked ? '#4a4a4a' : '#f0f0f0';
        lockRatioBtn.style.color = isRatioLocked ? '#fff' : '#000';
        lockRatioBtn.onclick = () => {
            isRatioLocked = !isRatioLocked;
            lockRatioBtn.style.background = isRatioLocked ? '#4a4a4a' : '#f0f0f0';
            lockRatioBtn.style.color = isRatioLocked ? '#fff' : '#000';
        };

        function updateInputs() {
            customWidth.value = selectedWidth;
            customHeight.value = selectedHeight;
            updateSizeInfo();
            updatePreview();
        }

        function updateSizeInfo() {
            const widthPercent = Math.round((selectedWidth / originalWidth) * 100);
            const heightPercent = Math.round((selectedHeight / originalHeight) * 100);
            sizeInfo.textContent = `当前: ${selectedWidth} × ${selectedHeight} (${widthPercent}%)
                               原始: ${originalWidth} × ${originalHeight}`;
        }

        function updatePreview() {
            if (previewImage) {
                // 添加加载提示
                previewImage.style.opacity = '0.3';
                previewImage.style.transition = 'opacity 0.3s';

                previewImage.onload = () => {
                    previewImage.style.opacity = '1';
                };

                previewImage.style.width = `${selectedWidth}px`;
                previewImage.style.height = `${selectedHeight}px`;

                // 调整预览区域的大小
                const maxPreviewWidth = Math.min(selectedWidth, window.innerWidth * 0.8);
                const maxPreviewHeight = Math.min(selectedHeight, 300);
                previewArea.style.width = `${maxPreviewWidth}px`;
                previewArea.style.height = `${maxPreviewHeight}px`;

                // 如果图片尺寸超过预览区域,添加提示
                if (selectedWidth > maxPreviewWidth || selectedHeight > maxPreviewHeight) {
                    previewArea.title = '图片已按比例缩放以适应预览窗口';
                } else {
                    previewArea.title = '';
                }
            }
        }

        function highlightButton(activeButton) {
            presetButtons.querySelectorAll('button').forEach(btn => {
                btn.style.background = '#f0f0f0';
                btn.style.color = '#000';
            });
            activeButton.style.background = '#4a4a4a';
            activeButton.style.color = '#fff';
        }

        function clearButtonHighlight() {
            presetButtons.querySelectorAll('button').forEach(btn => {
                btn.style.background = '#f0f0f0';
                btn.style.color = '#000';
            });
        }

        // 添加拖动调整大小功能
        function addResizeDrag(previewImage) {
            let isResizing = false;
            let startX, startY, startWidth, startHeight;

            previewImage.style.cursor = 'se-resize';

            previewImage.addEventListener('mousedown', (e) => {
                isResizing = true;
                startX = e.clientX;
                startY = e.clientY;
                startWidth = selectedWidth;
                startHeight = selectedHeight;

                document.addEventListener('mousemove', handleMouseMove);
                document.addEventListener('mouseup', () => {
                    isResizing = false;
                    document.removeEventListener('mousemove', handleMouseMove);
                }, { once: true });
            });

            function handleMouseMove(e) {
                if (!isResizing) return;

                const deltaX = e.clientX - startX;
                const deltaY = e.clientY - startY;

                if (isRatioLocked) {
                    // 保持宽高比
                    const ratio = Math.abs(deltaX) > Math.abs(deltaY) ? deltaX / startWidth : deltaY / startHeight;
                    selectedWidth = Math.max(10, Math.round(startWidth * (1 + ratio)));
                    selectedHeight = Math.max(10, Math.round(startHeight * (1 + ratio)));
                } else {
                    selectedWidth = Math.max(10, startWidth + deltaX);
                    selectedHeight = Math.max(10, startHeight + deltaY);
                }

                updateInputs();
            }
        }

        // 输入框事件处理
        customWidth.oninput = () => {
            const width = parseInt(customWidth.value);
            if (width > 0) {
                selectedWidth = width;
                if (isRatioLocked) {
                    selectedHeight = Math.round(width / aspectRatio);
                    customHeight.value = selectedHeight;
                }
                updateSizeInfo();
                updatePreview();
                clearButtonHighlight();
            }
        };

        customHeight.oninput = () => {
            const height = parseInt(customHeight.value);
            if (height > 0) {
                selectedHeight = height;
                if (isRatioLocked) {
                    selectedWidth = Math.round(height * aspectRatio);
                    customWidth.value = selectedWidth;
                }
                updateSizeInfo();
                updatePreview();
                clearButtonHighlight();
            }
        };

        // 按钮事件处理
        closeButton.onclick = () => {
            previewWindow.remove();
            lastSelectedText = '';
        };

        applyButton.onclick = () => {
            if (selectedWidth && selectedHeight) {
                const imgTagRegex = /\[img(?:=(\d+),(\d+))?\](https?:\/\/[^\s]+)\[\/img\]/;
                const match = selectedText.match(imgTagRegex);
                if (match) {
                    const imgURL = match[3];
                    const newCode = `[img=${selectedWidth},${selectedHeight}]${imgURL}[/img]`;
                    textarea.value = textarea.value.replace(selectedText, newCode);
                }
            }
            previewWindow.remove();
            lastSelectedText = '';
        };

        // 解析当前BBCode并获取图片信息
        const imgTagRegex = /\[img(?:=(\d+),(\d+))?\](https?:\/\/[^\s]+)\[\/img\]/;
        const match = selectedText.match(imgTagRegex);
        if (!match) {
            previewWindow.remove();
            return;
        }

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

        // 获取图片原始尺寸
        const tempImg = new Image();
        tempImg.onload = () => {
            originalWidth = tempImg.naturalWidth;
            originalHeight = tempImg.naturalHeight;
            aspectRatio = originalWidth / originalHeight;

            // 设置预览图片的源
            previewImage.src = tempImg.src;

            // 添加拖动调整大小功能
            addResizeDrag(previewImage);

            if (initialWidth && initialHeight) {
                selectedWidth = initialWidth;
                selectedHeight = initialHeight;
                const scale = initialWidth / originalWidth;
                const percentage = Math.round(scale * 100);
                updateInputs();
                // 选中对应预设按钮
                presetButtons.querySelectorAll('button').forEach((btn, index) => {
                    if (percentage === presets[index].value * 100) {
                        highlightButton(btn);
                    }
                });
            } else {
                selectedWidth = originalWidth;
                selectedHeight = originalHeight;
                updateInputs();
                highlightButton(presetButtons.lastElementChild); // 100%
            }
        };
        tempImg.src = imgURL;
    }

    // 监听文本选择变化
    const debouncedHandleSelectionChange = debounce(handleSelectionChange, 250);
    document.addEventListener('selectionchange', debouncedHandleSelectionChange);

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