Greasy Fork

Greasy Fork is available in English.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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