// ==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 // @downloadURL none // ==/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 = `${label}`; 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 = `



最近使用的方案:
`; 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 = '最近方案:
'; 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); })();