Yande.re 大图预览 显示艺术家 打开原图

Yande.re浏览增强:悬停大图预览,显示艺术家名称,点击跳转主页,右键名称标记艺术家,高亮显示不同颜色,支持双击/键盘翻页,自动显示隐藏图片,直接打开原图

// ==UserScript==
// @name         Yande.re 大图预览 显示艺术家 打开原图
// @version      1.2
// @description  Yande.re浏览增强:悬停大图预览,显示艺术家名称,点击跳转主页,右键名称标记艺术家,高亮显示不同颜色,支持双击/键盘翻页,自动显示隐藏图片,直接打开原图
// @description  原脚本已数年未更新,在原作者Joker(Streams)的基础上修改;  konachan有限支持(需自行添加@match),部分功能可能无法使用
// @author       Cylirix
// @author       Joker
// @author       Streams
// @match        https://yande.re/*
// @icon         https://yande.re/favicon.ico
// @grant        GM_getValue
// @grant        GM_setValue
// @namespace https://greasyfork.org/users/467741
// ==/UserScript==


jQuery.noConflict();
jQuery(function ($) {
    // 全局唯一标识系统
    var currentUUID = null;
    var requestController = null;
    var hoverTimer = null;
    var currentHoverItem = null;

    // 初始化艺术家状态(使用Tampermonkey存储系统)
    var artistStates = {};

    // 从Tampermonkey存储加载数据
    try {
        const savedStates = GM_getValue("artistStates", "{}");
        artistStates = JSON.parse(savedStates);
    } catch (e) {
        console.error("Error loading artist states:", e);
    }

    // 右键菜单容器
    const $contextMenu = $('<div id="artist-context-menu">').css({
        position: 'fixed',
        display: 'none',
        background: 'rgba(0,0,0,0.7)',
        borderRadius: '6px',
        zIndex: 10000,
        padding: '5px 5px'
    }).appendTo('body');

    // 状态配置 - 使用字母a、b、c作为ID
    const stateConfig = [
        {id: 'a', text: '已收藏', color: '#ffff00'},
        {id: 'b', text: '已下载', color: '#00ff11'},
        {id: 'c', text: 'ignore', color: '#999'},
        {id: 'clear', text: '清除', color: '#fff'}
    ];

    // 创建菜单项
    stateConfig.forEach(option => {
        $('<div class="menu-item">')
            .text(option.text)
            .css({
                padding: '5px 15px',
                cursor: 'pointer',
                color: option.color,
                fontSize: '12px'
            })
            .data('action', option.id)
            .hover(
                function() { $(this).css('background', '#444'); },
                function() { $(this).css('background', 'transparent'); }
            )
            .appendTo($contextMenu);
    });

    // 预览框容器
    var $zoombox = $('<div id="zoombox">').css({
        position: 'fixed',
        top: 0,
        left: 0,
        'z-index': 9999,
        'pointer-events': 'none',
        display: 'none',
        'overflow': 'hidden',
        'transition': 'width 0.3s, height 0.3s'
    }).appendTo('body');

    // 添加全局样式
    function addGlobalStyle(css) {
        $('<style></style>').html(css).appendTo('head');
    }

    // 动态生成状态样式
    let stateStyles = '';
    stateConfig.forEach(state => {
        if (state.id !== 'clear') {
            stateStyles += `
                .artist-label.status-${state.id} {
                    color: ${state.color} !important;
                }
            `;

            // 特殊状态样式
            if (state.id === 'c') {
                stateStyles += `
                    .artist-label.status-c {
                        text-decoration: line-through;
                    }
                `;
            }
        }
    });

    addGlobalStyle(`
        /* 主预览框样式 */
        #zoombox {
            max-width: 100%;
            max-height: 100%;
            border-radius: 4px;
        }

        /* 预览图内部样式 */
        #zoombox img {
            display: block;
            pointer-events: none;
            object-fit: contain;
            transition: opacity 0.3s;
            border-radius: 4px;
        }

        /* 图片容器 */
        .inner {
            position: relative;
        }

        /* 控制容器 - 右上角 */
        .control-box {
            position: absolute;
            top: 0px;
            right: 0px;
            z-index: 1000;
            pointer-events: none;
            text-align: right;
            max-width: 80%;  /* 限制最大宽度 */
            font-size: 11px; /* 统一字体大小 */
            line-height: 1.2;
            display: flex;         /* 使用flex布局 */
            flex-direction: column; /* 垂直排列元素 */
            align-items: flex-end;  /* 内容右对齐 */
        }

        /* 艺术家标签 - 多行显示 */
        .artist-label {
            background: rgba(0,0,0,0.3);
            color: white; /* 默认文字颜色 */
            padding: 2px 5px;
            border-radius: 3px;
            font-weight: bold;
            white-space: normal;   /* 允许多行显示 */
            word-break: break-word; /* 长单词换行 */
            pointer-events: auto;
            margin-bottom: 2px;   /* 与下方按钮间距 */
            max-width: 100%;      /* 宽度限制 */
            text-align: left;     /* 内部文本左对齐 */
            cursor: context-menu; /* 显示右键菜单光标 */
        }

        /* unknown状态特殊样式 */
        .artist-label.unknown {
            color: #aaa !important; /* 更灰暗的颜色 */
            font-style: italic; /* 添加斜体效果 */
        }

        /* 艺术家状态样式 */
        ${stateStyles}

        /* 艺术家链接样式 */
        .artist-label a {
            color: inherit; /* 继承父元素颜色 */
            text-decoration: none;
        }
        .artist-label a:hover {
            text-decoration: underline;
        }

        /* 原图按钮 - 背景大小优化 */
        .original-btn {
            background: rgba(0,0,0,0.35);
            color: white;
            padding: 2px 5px;
            border-radius: 3px;
            text-decoration: none;
            font-weight: bold;
            pointer-events: auto;
            white-space: nowrap;   /* 禁止文字换行 */
            display: none;         /* 默认隐藏 */
        }

        /* 悬停时显示原图按钮 */
        .inner:hover .original-btn {
            display: block;        /* 显示为块级元素,独占一行 */
        }

        /* 原图按钮悬停效果 */
        .original-btn:hover {
            background: rgba(200, 80, 80, 0.8);
        }

        /* 右键菜单样式 */
        #artist-context-menu {
            font-family: Arial, sans-serif;
        }
        .menu-item {
            transition: background 0.5s;
        }
        /* === 默认显示分辨率 === */
        #post-list-posts li a.directlink span.directlink-info {
            display: none !important;
        }
        #post-list-posts li a.directlink span.directlink-res {
            display: inline !important;
        }

    `);

    // 翻页功能
    function addKey() {
        $(document).dblclick(function (e) {
            var w = document.documentElement.offsetWidth || document.body.offsetWidth;
            if (e.clientX > w / 2) nextPage();
            else previousPage();
        });
        $(document).keydown(function (e) {
            if (e.keyCode == 37) previousPage();
            else if (e.keyCode == 39) nextPage();
        });
        function nextPage() {
            var $nextBtn = $('a.next_page');
            if ($nextBtn.length > 0) $nextBtn[0].click();
        }
        function previousPage() {
            var $preBtn = $('a.previous_page');
            if ($preBtn.length > 0) $preBtn[0].click();
        }
    }

    // 生成唯一标识符
    function generateUUID() {
        return Date.now().toString(36) + Math.random().toString(36).substr(2);
    }


    function setupContextMenu() {
        // 右键点击事件
        $(document).on('contextmenu', '.artist-label', function(e) {
            e.preventDefault();

            const artistName = $(this).text().trim();
            if (!artistName || artistName === 'unknown') return;

            // === 直接使用视口坐标 ===
            const x = e.clientX;
            const y = e.clientY;

            // 显示菜单
            $contextMenu
                .data('artist', artistName)
                .css({
                display: 'block',
                left: x + 'px',
                top: y + 'px'
            });

            // === 添加边界检查 ===
            setTimeout(() => {
                const menuRect = $contextMenu[0].getBoundingClientRect();
                const windowWidth = window.innerWidth;
                const windowHeight = window.innerHeight;

                let adjustedX = x;
                let adjustedY = y;

                // 检查右边界
                if (menuRect.right > windowWidth) {
                    adjustedX = windowWidth - menuRect.width - 10;
                }

                // 检查下边界
                if (menuRect.bottom > windowHeight) {
                    adjustedY = windowHeight - menuRect.height - 10;
                }

                // 如果需要调整位置
                if (adjustedX !== x || adjustedY !== y) {
                    $contextMenu.css({
                        left: adjustedX + 'px',
                        top: adjustedY + 'px'
                    });
                }
            }, 0);
        });

        // 菜单项点击事件
        $contextMenu.on('click', '.menu-item', function() {
            const action = $(this).data('action');
            const artistName = $contextMenu.data('artist');

            if (action === 'clear') {
                // 清除标记
                delete artistStates[artistName];
            } else {
                // 设置状态
                artistStates[artistName] = action;
            }

            // 保存到Tampermonkey存储
            GM_setValue("artistStates", JSON.stringify(artistStates));

            // 更新所有该艺术家的标签
            $(`.artist-label:contains('${artistName}')`).each(function() {
                const $label = $(this);

                // 清除所有状态类
                stateConfig.forEach(state => {
                    if (state.id !== 'clear') {
                        $label.removeClass(`status-${state.id}`);
                    }
                });

                // 添加新状态类
                if (action !== 'clear' && artistStates[artistName]) {
                    $label.addClass(`status-${artistStates[artistName]}`);
                }
            });

            // 隐藏菜单
            $contextMenu.hide();
        });

        // 点击其他地方关闭菜单
        $(document).on('click', function(e) {
            if (!$(e.target).closest('#artist-context-menu').length) {
                $contextMenu.hide();
            }
        });

    }
    // 优化的预览逻辑
    function addMouseZoomPreview() {
        $("#post-list-posts > li, .pool-show .inner").each(function() {
            const $item = $(this);
            const $inner = $item.find('.inner');
            if ($inner.length === 0) return;

            let postId = $item.data('id');
            if (!postId) {
                // If not found, try to extract from thumb link
                const $thumbLink = $inner.find('a.thumb');
                if ($thumbLink.length) {
                    const href = $thumbLink.attr('href');
                    const postIdMatch = href && href.match(/\/post\/show\/(\d+)/);
                    if (postIdMatch && postIdMatch[1]) {
                        postId = postIdMatch[1];
                    }
                }
            }

            // 创建控制容器(艺术家标签和原图按钮的父元素)
            const $controlBox = $('<div class="control-box"></div>');
            $inner.append($controlBox);

            // 创建原图按钮
            let originalUrl = null;
            let sampleUrl = null;
            const $thumbLink = $inner.find('a.thumb');

            // === 获取当前域名 ===
            const currentDomain = window.location.hostname;
            const isKonachan = currentDomain.includes('konachan');
            const isYandere = currentDomain.includes('yande.re');

            // 方法1:从pool页面结构获取URL
            if ($thumbLink.length) {
                const href = $thumbLink.attr('href');
                const postIdMatch = href && href.match(/\/post\/show\/(\d+)/);

                if (postIdMatch && postIdMatch[1]) {
                    const postId = postIdMatch[1];
                    const thumbSrc = $thumbLink.find('img').attr('src');
                    if (thumbSrc) {
                        const md5 = thumbSrc.split('/').pop().split('.')[0];
                        if (md5) {
                            // === 根据域名动态生成URL ===
                            if (isYandere) {
                                originalUrl = `https://files.yande.re/image/${md5}/yande.re%20${postId}.jpg`;
                                sampleUrl = `https://files.yande.re/sample/${md5}/yande.re%20${postId}%20sample.jpg`;
                            } else if (isKonachan) {
                                originalUrl = `https://${currentDomain}/image/${md5}.jpg`;
                                sampleUrl = `https://${currentDomain}/sample/${md5}.jpg`;
                            }
                        }
                    }
                }
            }

            // 方法2:从普通页面结构获取URL(仅当方法1失败时尝试)
            if (!originalUrl || !sampleUrl) {
                const $directLink = $item.find('a[class*="directlink"][href]');
                if ($directLink.length) {
                    const url = $directLink.attr('href');
                    if (url) {
                        originalUrl = url;

                        // === 根据域名动态生成sample URL ===
                        if (isYandere) {
                            if (url.includes('/jpeg/')) {
                                originalUrl = url.replace('/jpeg/', '/image/');
                            }
                            if (url.includes('/image/')) {
                                sampleUrl = url.replace('/image/', '/sample/');
                            } else if (url.includes('/jpeg/')) {
                                sampleUrl = url.replace('/jpeg/', '/sample/');
                            } else {
                                sampleUrl = url;
                            }
                        } else if (isKonachan) {
                            // Konachan的URL格式不同
                            if (url.includes('/image/')) {
                                sampleUrl = url.replace('/image/', '/sample/');
                            } else {
                                // 尝试从URL中提取md5
                                const md5Match = url.match(/\/([a-f0-9]{32})\.jpg$/);
                                if (md5Match && md5Match[1]) {
                                    sampleUrl = `https://${currentDomain}/sample/${md5Match[1]}.jpg`;
                                }
                            }
                        }
                    }
                }
            }

            // 如果无法获取URL则跳过
            if (!originalUrl || !sampleUrl) return;

            // 创建并添加原图按钮 - 默认隐藏
            const $originalBtn = $(`<a class="original-btn" href="${originalUrl}" target="_blank">打开原图</a>`);
            $controlBox.append($originalBtn);

            // 获取艺术家信息
            if (postId) {
                $.get(`/post.json?api_version=2&include_tags=1&tags=id:${postId}`, function(data) {
                    if (data.posts && data.posts.length > 0) {
                        const post = data.posts[0];
                        const tagTypes = data.tags || {};
                        const tags = post.tags.split(' ');
                        const artists = tags.filter(tag => tagTypes[tag] === 'artist');
                        let artistText = artists.length > 0 ? artists.join(', ') : 'unknown';

                        // 创建艺术家标签
                        const $artistLabel = $('<div class="artist-label"></div>');

                        // 检查是否为unknown状态
                        const isUnknown = artistText === 'unknown';
                        if (isUnknown) {
                            $artistLabel.addClass('unknown');
                        }

                        // 处理艺术家标签(单艺术家可点击,多艺术家只显示)
                        if (artists.length === 1) {
                            const artistName = artists[0];
                            // 创建艺术家链接
                            $artistLabel.append(
                                $(`<a href="/post?tags=${encodeURIComponent(artistName)}" target="_blank"></a>`)
                                .text(artistName)
                            );
                            // === 使用TM存储的状态 ===
                            if (artistStates[artistName]) {
                                $artistLabel.addClass(`status-${artistStates[artistName]}`);
                            }

                        } else {
                            $artistLabel.text(artistText);
                        }

                        // 添加到控制容器顶部
                        $controlBox.prepend($artistLabel);
                    }
                });
            }

            // 获取缩略图元素
            const $thumbImg = $item.find('img.preview');
            if ($thumbImg.length === 0) return;

            // 获取预设的图片宽高比
            const thumbWidth = $thumbImg.width();
            const thumbHeight = $thumbImg.height();
            const aspectRatio = thumbWidth / thumbHeight;

            // 悬停事件
            $item.hover(
                // 鼠标进入
                function (e) {
                    // 保存当前悬停的元素
                    currentHoverItem = this;

                    // 清除之前的延时器
                    if (hoverTimer) {
                        clearTimeout(hoverTimer);
                        hoverTimer = null;
                    }

                    // 设置延时器(300毫秒)
                    hoverTimer = setTimeout(() => {
                        // 检查是否仍在同一个元素上
                        if (currentHoverItem !== this) return;

                        // 终止可能存在的旧请求
                        if (requestController) {
                            requestController.abort();
                        }

                        // 生成新UUID作为当前悬停标识
                        const thisUUID = generateUUID();
                        currentUUID = thisUUID;
                        requestController = new AbortController();

                        // 获取屏幕尺寸
                        const screenWidth = $(window).width();
                        const screenHeight = $(window).height();

                        // 判断横竖图
                        const isVertical = aspectRatio <= 1;

                        // 根据宽高比设置初始尺寸
                        let initWidth, initHeight;
                        if (isVertical) {
                            // 竖图:高度占满屏幕高度(无上下边距)
                            initHeight = screenHeight;
                            initWidth = initHeight * aspectRatio;
                        } else {
                            // 横图:宽度占屏幕宽度50%
                            initWidth = Math.min(screenWidth * 0.5, screenHeight * aspectRatio);
                            initHeight = initWidth / aspectRatio;
                        }

                        // 避开鼠标位置的逻辑
                        const mouseX = e.clientX;
                        let positionLeft = 'auto';
                        let positionRight = '10px';

                        if (mouseX < screenWidth / 2) {
                            // 鼠标在左侧,显示在右侧
                            positionRight = '10px';
                            positionLeft = 'auto';
                        } else {
                            // 鼠标在右侧,显示在左侧
                            positionLeft = '10px';
                            positionRight = 'auto';
                        }

                        // 应用位置
                        $zoombox.css({
                            top: '5px',
                            left: positionLeft,
                            right: positionRight,
                            width: initWidth + 'px',
                            height: initHeight + 'px'
                        });

                        // 创建加载容器
                        const $imgContainer = $('<div>').css({
                            position: 'relative',
                            overflow: 'hidden',
                            width: '100%',
                            height: '100%'
                        });

                        const $img = $('<img>').css({
                            opacity: 1,
                            width: '100%',
                            height: '100%'
                        });

                        $imgContainer.append($img);
                        $zoombox.empty().append($imgContainer).show();

                        // 绑定加载信号
                        requestController.signal.addEventListener('abort', () => {
                            if (currentUUID !== thisUUID) return;
                            $zoombox.hide().empty();
                        });

                        // 加载完成的回调
                        $img.on('load', function() {
                            if (currentUUID !== thisUUID) return;

                            // 获取实际图片信息
                            const naturalWidth = this.naturalWidth;
                            const naturalHeight = this.naturalHeight;
                            const actualAspectRatio = naturalWidth / naturalHeight;

                            // 判断实际图片横竖图
                            const isActualVertical = actualAspectRatio <= 1;

                            // 最终尺寸调整
                            if (isActualVertical) {
                                // 竖图:高度占满屏幕高度(无上下边距)
                                const finalHeight = screenHeight;
                                const finalWidth = finalHeight * actualAspectRatio;

                                $zoombox.css({
                                    width: finalWidth + 'px',
                                    height: finalHeight + 'px'
                                });
                            } else {
                                // 横图:宽度占屏幕宽度50%
                                const finalWidth = screenWidth * 0.5;
                                const finalHeight = finalWidth / actualAspectRatio;

                                $zoombox.css({
                                    width: finalWidth + 'px',
                                    height: finalHeight + 'px'
                                });
                            }
                        });

                        $img.on('error', function() {
                            if (currentUUID !== thisUUID) return;
                            $img.attr('src', originalUrl);
                        });

                        // 开始加载图片
                        $img.attr('src', sampleUrl);
                    }, 300); // 300毫秒延时
                },

                // 鼠标移出
                function () {
                    // 清除延时器
                    if (hoverTimer) {
                        clearTimeout(hoverTimer);
                        hoverTimer = null;
                    }

                    // 重置当前悬停元素
                    currentHoverItem = null;

                    // 终止加载请求
                    if (requestController) {
                        requestController.abort();
                        requestController = null;
                    }

                    $zoombox.hide().empty();
                }
            );
        });
    }

    // 显示隐藏图片
    function showHiddenImage() {
        $("#post-list-posts > li.javascript-hide").removeClass("javascript-hide");
    }

    // 独立函数:禁止图片气泡消息(移除title属性)
    function disableImageTooltips() {
        // 针对列表页的图片缩略图移除title属性
        $("#post-list-posts img.preview[title]").removeAttr("title");
        // 如果有其他可能的图片元素,可以在这里扩展选择器
    }

    // 初始化操作
    $(document).ready(function() {
        setTimeout(function() {
            showHiddenImage();
            addKey();
            addMouseZoomPreview();
            setupContextMenu(); // 初始化右键菜单功能
            disableImageTooltips(); // 禁止气泡消息
        }, 500);
    });
});