Bangumi 图片上传增强

支持自由比例裁切+顶部对齐正方形预览

目前為 2025-08-13 提交的版本,檢視 最新版本

// ==UserScript==
// @name         Bangumi 图片上传增强
// @namespace    https://bgm.tv/group/topic/431819
// @version      1.3
// @description  支持自由比例裁切+顶部对齐正方形预览
// @author       You
// @match        https://bangumi.tv/character/*/upload_photo
// @match        https://bangumi.tv/character/*/upload_img
// @match        https://bangumi.tv/person/*/upload_photo
// @match        https://bangumi.tv/person/*/upload_img
// @match        https://bangumi.tv/subject/*/upload_img
// @match        https://bgm.tv/character/*/upload_photo
// @match        https://bgm.tv/character/*/upload_img
// @match        https://bgm.tv/person/*/upload_photo
// @match        https://bgm.tv/person/*/upload_img
// @match        https://bgm.tv/subject/*/upload_img
// @match        https://chii.in/character/*/upload_photo
// @match        https://chii.in/character/*/upload_img
// @match        https://chii.in/person/*/upload_photo
// @match        https://chii.in/person/*/upload_img
// @match        https://chii.in/subject/*/upload_img
// @grant        GM_xmlhttpRequest
// @grant        GM_getResourceText
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/cropper.min.js
// @resource     cropperCSS https://cdn.jsdelivr.net/npm/[email protected]/dist/cropper.min.css
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    // 注入所有CSS样式
    const css = `
        .bgm-uploader-container {
            margin-top: 15px;
        }

        .bgm-preview-container {
            display: flex;
            gap: 15px;
            margin-bottom: 10px;
            flex-wrap: wrap;
            align-items: flex-start;
        }

        .bgm-original-preview {
            max-width: 300px;
            display: none;
        }

        .bgm-square-preview {
            display: none;
            width: 100px;
        }

        .bgm-preview-image {
            max-width: 100%;
            max-height: 200px;
            border: 1px solid #ddd;
        }

        /* 静态正方形预览容器 */
        .bgm-square-image-container {
            width: 100px;
            height: 100px;
            overflow: hidden;
            border: 1px solid #ddd;
        }

        /* 静态正方形预览图片 - 顶部对齐 */
        .bgm-square-image {
            width: 100%;
            height: 100%;
            object-fit: cover;
            object-position: top center; /* 关键修改:确保从顶部开始显示 */
        }

        /* 裁切过程中的正方形预览 */
        .bgm-cropper-preview {
            width: 100px;
            height: 100px;
            overflow: hidden;
            display: none;
            border: 1px solid #ddd;
            position: relative;
        }

        .bgm-cropper-preview-image {
            position: absolute;
            left: 0;
            transform-origin: left top; /* 关键修改:从左上角开始变换 */
        }

        .bgm-preview-text {
            margin-top: 5px;
            font-size: 0.8em;
            color: #666;
        }

        .bgm-controls {
            margin-top: 10px;
            display: none;
        }

        .bgm-hint {
            color: #666;
            font-size: 0.9em;
            margin-bottom: 10px;
        }
    `;

    const style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    // 添加Cropper.js CSS
    const cropperCSS = GM_getResourceText('cropperCSS');
    const cropperStyle = document.createElement('style');
    cropperStyle.textContent = cropperCSS;
    document.head.appendChild(cropperStyle);

    const fileInput = document.querySelector('input[type="file"][name="picfile"]');
    if (!fileInput) return;

    // 检查是否是/upload_photo页面
    const isUploadPhotoPage = window.location.pathname.includes('/upload_photo');

    // 创建UI容器
    const container = document.createElement('div');
    container.className = 'bgm-uploader-container';

    // 预览容器(使用flex布局)
    const previewWrapper = document.createElement('div');
    previewWrapper.className = 'bgm-preview-container';

    // 原始预览
    const originalPreview = document.createElement('div');
    originalPreview.className = 'bgm-original-preview';

    const previewImage = document.createElement('img');
    previewImage.className = 'bgm-preview-image';

    const previewText = document.createElement('div');
    previewText.className = 'bgm-preview-text';

    // 正方形预览(仅在/upload_photo页面显示)
    let squarePreview = null;
    let squarePreviewImage = null;

    // 裁切过程中的强制正方形预览
    let cropperPreview = null;
    let cropperPreviewImage = null;

    if (isUploadPhotoPage) {
        // 静态正方形预览
        squarePreview = document.createElement('div');
        squarePreview.className = 'bgm-square-preview';

        const squarePreviewLabel = document.createElement('div');
        squarePreviewLabel.textContent = '正方形预览:';
        squarePreviewLabel.className = 'bgm-preview-text';

        const squareImageContainer = document.createElement('div');
        squareImageContainer.className = 'bgm-square-image-container';

        squarePreviewImage = document.createElement('img');
        squarePreviewImage.className = 'bgm-square-image';

        squareImageContainer.appendChild(squarePreviewImage);
        squarePreview.appendChild(squarePreviewLabel);
        squarePreview.appendChild(squareImageContainer);

        // 裁切过程中的正方形预览
        cropperPreview = document.createElement('div');
        cropperPreview.className = 'bgm-cropper-preview';

        const cropperPreviewLabel = document.createElement('div');
        cropperPreviewLabel.textContent = '裁切预览:';
        cropperPreviewLabel.className = 'bgm-preview-text';

        cropperPreviewImage = document.createElement('img');
        cropperPreviewImage.className = 'bgm-cropper-preview-image';

        cropperPreview.appendChild(cropperPreviewLabel);
        cropperPreview.appendChild(cropperPreviewImage);
    }

    // 控制按钮
    const controls = document.createElement('div');
    controls.className = 'bgm-controls';

    const cropBtn = document.createElement('button');
    cropBtn.textContent = '裁切图片';

    const confirmBtn = document.createElement('button');
    confirmBtn.textContent = '确认裁切';
    confirmBtn.style.display = 'none';

    const cancelBtn = document.createElement('button');
    cancelBtn.textContent = '取消裁切';
    cancelBtn.style.display = 'none';

    // 组装UI
    originalPreview.appendChild(previewImage);
    originalPreview.appendChild(previewText);
    previewWrapper.appendChild(originalPreview);

    if (isUploadPhotoPage) {
        if (squarePreview) previewWrapper.appendChild(squarePreview);
        if (cropperPreview) previewWrapper.appendChild(cropperPreview);
    }

    controls.appendChild(cropBtn);
    controls.appendChild(confirmBtn);
    controls.appendChild(cancelBtn);
    container.appendChild(previewWrapper);
    container.appendChild(controls);
    fileInput.parentNode.insertBefore(container, fileInput.nextSibling);

    // 支持的图片后缀
    const imageExtensions = ['jpg', 'png', 'gif'];
    let currentFile = null;
    let cropper = null;
    let isCropping = false;

    // 检查是否是图片URL
    function isImageUrl(text) {
        try {
            const url = new URL(text);
            const ext = url.pathname.split('.').pop().toLowerCase();
            return imageExtensions.includes(ext);
        } catch {
            return false;
        }
    }

    // 从URL获取图片文件
    function fetchImageFromUrl(url) {
        previewText.textContent = '正在下载图片...';
        originalPreview.style.display = 'block';
        if (squarePreview) squarePreview.style.display = 'none';
        if (cropperPreview) cropperPreview.style.display = 'none';

        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: 'GET',
                url: url,
                responseType: 'blob',
                onload: function(response) {
                    const blob = response.response;
                    const fileName = url.split('/').pop();
                    const file = new File([blob], fileName, { type: blob.type });
                    resolve(file);
                },
                onerror: function(error) {
                    reject(new Error('图片下载失败: ' + error.statusText));
                }
            });
        });
    }

    // 更新强制正方形预览(从顶部开始显示)
    function updateSquarePreview() {
        if (!cropper || !cropperPreviewImage) return;

        const canvas = cropper.getCroppedCanvas();
        if (!canvas) return;

        // 计算缩放比例以适应正方形(保持顶部对齐)
        const scale = 100 / canvas.width; // 基于宽度缩放

        // 更新预览图片
        cropperPreviewImage.src = canvas.toDataURL();
        cropperPreviewImage.style.width = `${canvas.width * scale}px`;
        cropperPreviewImage.style.height = `${canvas.height * scale}px`;

        // 顶部对齐,水平居中
        cropperPreviewImage.style.top = '0';
        cropperPreviewImage.style.left = '50%';
        cropperPreviewImage.style.transform = 'translateX(-50%)';
    }

    // 初始化裁切工具
    function initCropper() {
        if (cropper) {
            cropper.destroy();
        }

        // 显示裁切预览,隐藏静态预览
        if (cropperPreview) {
            cropperPreview.style.display = 'block';
        }
        if (squarePreview) {
            squarePreview.style.display = 'none';
        }

        cropper = new Cropper(previewImage, {
            viewMode: 1,
            autoCropArea: 0.8,
            responsive: true,
            movable: true,
            rotatable: true,
            scalable: true,
            zoomable: true,
            crop: function() {
                // 裁切时更新强制正方形预览
                updateSquarePreview();
            }
        });

        // 初始更新一次预览
        updateSquarePreview();
    }

    // 开始裁切
    function startCropping(e) {
        e.preventDefault();
        if (!currentFile) return;

        isCropping = true;
        initCropper();
        cropBtn.style.display = 'none';
        confirmBtn.style.display = 'inline-block';
        cancelBtn.style.display = 'inline-block';
        previewText.textContent = '拖动边框调整裁切区域,然后点击"确认裁切"';
    }

    // 确认裁切
    function confirmCrop(e) {
        e.preventDefault();
        if (!cropper || !isCropping) return;

        cropper.getCroppedCanvas({
            fillColor: 'transparent'
        }).toBlob(blob => {
            const fileName = currentFile.name.replace(/\.[^/.]+$/, '') + '_cropped.png';
            currentFile = new File([blob], fileName, { type: 'image/png' });

            // 更新文件输入
            const dataTransfer = new DataTransfer();
            dataTransfer.items.add(currentFile);
            fileInput.files = dataTransfer.files;

            // 更新预览
            updatePreview(currentFile);
            endCropping();

            // 恢复显示静态正方形预览
            if (squarePreview) {
                squarePreview.style.display = 'block';
            }
            if (cropperPreview) {
                cropperPreview.style.display = 'none';
            }
        }, 'image/png');
    }

    // 取消裁切
    function cancelCrop(e) {
        e.preventDefault();
        if (!isCropping) return;
        updatePreview(currentFile);
        endCropping();

        // 恢复显示静态正方形预览
        if (squarePreview) {
            squarePreview.style.display = 'block';
        }
        if (cropperPreview) {
            cropperPreview.style.display = 'none';
        }
    }

    // 结束裁切模式
    function endCropping() {
        isCropping = false;
        if (cropper) {
            cropper.destroy();
            cropper = null;
        }
        cropBtn.style.display = 'inline-block';
        confirmBtn.style.display = 'none';
        cancelBtn.style.display = 'none';
    }

    // 更新预览
    function updatePreview(file) {
        if (!file) {
            originalPreview.style.display = 'none';
            controls.style.display = 'none';
            if (squarePreview) squarePreview.style.display = 'none';
            if (cropperPreview) cropperPreview.style.display = 'none';
            return false;
        }

        const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
        if (!allowedTypes.includes(file.type)) {
            previewText.textContent = `错误:不支持的文件格式 (${file.type}),请上传 JPG/PNG/GIF 图片`;
            previewImage.style.display = 'none';
            originalPreview.style.display = 'block';
            controls.style.display = 'none';
            if (squarePreview) squarePreview.style.display = 'none';
            if (cropperPreview) cropperPreview.style.display = 'none';
            return false;
        }

        const reader = new FileReader();
        reader.onload = function(e) {
            previewImage.src = e.target.result;
            previewImage.style.display = 'block';
            previewText.textContent = `已选择: ${file.name} (${Math.round(file.size/1024)}KB)`;
            originalPreview.style.display = 'block';
            controls.style.display = 'block';

            // 更新静态正方形预览(仅在非裁切模式下显示)
            if (isUploadPhotoPage && squarePreviewImage && !isCropping) {
                squarePreviewImage.src = e.target.result;
                squarePreview.style.display = 'block';
                if (cropperPreview) cropperPreview.style.display = 'none';
            }

            // 重置裁切状态
            endCropping();
        };
        reader.readAsDataURL(file);
        currentFile = file;
        return true;
    }

    // 事件监听
    cropBtn.addEventListener('click', startCropping);
    confirmBtn.addEventListener('click', confirmCrop);
    cancelBtn.addEventListener('click', cancelCrop);

    // 监听文件输入变化
    fileInput.addEventListener('change', function() {
        if (this.files.length > 0) {
            updatePreview(this.files[0]);
        }
    });

    // 监听粘贴事件
    document.addEventListener('paste', async function(e) {
        // 检查剪贴板中的图片
        if (e.clipboardData.items && e.clipboardData.items.length > 0) {
            for (let i = 0; i < e.clipboardData.items.length; i++) {
                const item = e.clipboardData.items[i];
                if (item.type.indexOf('image') !== -1) {
                    const blob = item.getAsFile();
                    if (updatePreview(blob)) {
                        const dataTransfer = new DataTransfer();
                        dataTransfer.items.add(blob);
                        fileInput.files = dataTransfer.files;
                    }
                    e.preventDefault();
                    return;
                }
            }
        }

        // 检查剪贴板中的文本(图片URL)
        const pastedText = e.clipboardData.getData('text/plain').trim();
        if (isImageUrl(pastedText)) {
            e.preventDefault();
            try {
                const imageFile = await fetchImageFromUrl(pastedText);
                if (updatePreview(imageFile)) {
                    const dataTransfer = new DataTransfer();
                    dataTransfer.items.add(imageFile);
                    fileInput.files = dataTransfer.files;
                }
            } catch (error) {
                previewText.textContent = error.message;
                previewImage.style.display = 'none';
                originalPreview.style.display = 'block';
                if (squarePreview) squarePreview.style.display = 'none';
                if (cropperPreview) cropperPreview.style.display = 'none';
            }
        }
    });

    // 添加提示信息
    const hint = document.createElement('div');
    hint.className = 'bgm-hint';
    hint.textContent = '提示:可直接粘贴图片或图片链接';
    fileInput.parentNode.insertBefore(hint, fileInput.nextSibling);
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址