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