Greasy Fork

Greasy Fork is available in English.

bgm相关回复跳转

在web自动识别贴贴过的帖子,提供跳转。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         bgm相关回复跳转
// @version      0.5
// @description  在web自动识别贴贴过的帖子,提供跳转。
// @match        https://bgm.tv/ep/*
// @match        https://bangumi.tv/ep/*
// @match        https://chii.in/ep/*
// @grant        none
// @license      MIT
// @namespace    bgm_jump_related_post
// @require      https://cdn.tailwindcss.com
// ==/UserScript==


// 几个todo,目前自己没这个需求,暂时简化。如果有人需要可以说一声:
// - [ ] 除了贴贴也支持回复过的跳转。
// 前期主要支持贴贴跳转
// 后续会增加回复跳转的,因为其实可以搜索,相对没那么着急
// - [ ] 暂时只支持番剧吐槽页面,主要是觉得别的页面也没必要。
// - [x] 暂时只处理每层回复的一楼,这个主要是暂时懒得做先简化一下。也就是不支持比如说第二楼的所有回复贴贴。
// - [ ] 看要不要支持这个框可拖动,要不就是现在这个折叠之后的样式有点小奇怪,就是不应该留东西太外面,这个动画有点太花哨了。整体其实可以更简单一点,写到后面有点后悔了,引入样式库其实已经是极限了,后续应该更精简一点

(function () {
    'use strict';

    // Load Tailwind CSS
    const tailwindScript = document.createElement('script');
    tailwindScript.src = 'https://cdn.tailwindcss.com';
    document.head.appendChild(tailwindScript);

    const selfUserId = $('.avatar').attr('href').split('/').pop();
    // 先完成遍历处理第一层的逻辑
    const replyList = $('#comment_list>div');  // 得到所有首层回复
    let relatedPostIds = [];  // 筛选最终跟当前用户相关的id

    // replyList.slice(0, 2).each(function () {  // 调试场景只看前三个帖子
    replyList.each(function () {  // 逐个过滤
        const firstLevelId = $(this).attr('id');
        
        // 检查一楼
        if (checkReplyElementRelated(selfUserId, $(this))) {
            relatedPostIds.push(firstLevelId);
        }
        
        // 检查楼中楼(二级回复)
        const subReplies = $(this).find('.topic_sub_reply .sub_reply_bg');
        subReplies.each(function() {
            if (checkSubReplyRelated(selfUserId, $(this))) {
                const subReplyId = $(this).attr('id');
                relatedPostIds.push(subReplyId);
            }
        });
    });

    // console.log(relatedPostIds);
    const component = createJumpComponent(relatedPostIds);
    const columnEPB = $('#columnEpB')
    columnEPB.append(component);
    
    // Initialize component behavior after DOM insertion
    initializeComponent(relatedPostIds);
})();

// 检查当前回复元素的「贴贴」里边是否包含当前用户id
function checkReplyElementRelated(userId, element) {
    // 先在这个元素内继续选择 `a.item.selected`
    // const likesGridItem = element.find('a.item.selected');
    const innerDiv = element.children().eq(2)

    // 这个元素里的 title 包含所有 贴贴元素的 HTML 文本,每个 `<a>` 就是一种类型的贴贴
    const likesGridItemAs = innerDiv.find('.reply_content').children().eq(1).children();
    if (likesGridItemAs.length === 0) {
        return false;
    }

    let found = false;
    likesGridItemAs.each(function () {
        const title = $(this).attr('title');
        if (checkUserIdInTitle(userId, title)) {
            found = true;
            return false;  // break loop
            // ! 这里才是坑,里边有个回调函数,return true 会直接返回到这个回调函数,而不是外层的函数
        }
    });

    return found;
}

function checkUserIdInTitle(userId, title) {
    const regex = /\/user\/(\w+)/g;
    let match;
    while ((match = regex.exec(title)) !== null) {
        if (match[1] === userId) {
            return true;
        }
    }
    return false;
}

// 检查楼中楼(二级回复)是否包含当前用户的贴贴
function checkSubReplyRelated(userId, element) {
    // 二级回复的贴贴在 .likes_grid 中
    const likesGrid = element.find('.likes_grid');
    if (likesGrid.length === 0) {
        return false;
    }
    
    const likesGridItems = likesGrid.find('a.item');
    if (likesGridItems.length === 0) {
        return false;
    }
    
    let found = false;
    likesGridItems.each(function () {
        const title = $(this).attr('title');
        if (title && checkUserIdInTitle(userId, title)) {
            found = true;
            return false;  // break loop
        }
    });
    
    return found;
}

function createJumpComponent(postIds) {
    const isEmpty = postIds.length === 0;
    
    // Apple-inspired Bento Grid styling with Tailwind + Custom CSS
    const styles = `
        <style>
            @import url('https://fonts.googleapis.com/css2?family=SF+Pro+Display:wght@400;500;600;700&display=swap');
            
            /* Custom properties for Apple-style design */
            :root {
                --bento-bg: rgba(255, 255, 255, 0.72);
                --bento-border: rgba(0, 0, 0, 0.04);
                --bento-shadow: 0 8px 32px rgba(0, 0, 0, 0.08);
                --bento-text: #1d1d1f;
                --bento-text-secondary: #86868b;
                --bento-accent: #0071e3;
                --bento-accent-hover: #0077ed;
                --bento-item-bg: rgba(255, 255, 255, 0.5);
                --bento-item-hover: rgba(245, 245, 247, 0.9);
            }
            
            @media (prefers-color-scheme: dark) {
                :root {
                    --bento-bg: rgba(29, 29, 31, 0.72);
                    --bento-border: rgba(255, 255, 255, 0.1);
                    --bento-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
                    --bento-text: #f5f5f7;
                    --bento-text-secondary: #a1a1a6;
                    --bento-accent: #0a84ff;
                    --bento-accent-hover: #409cff;
                    --bento-item-bg: rgba(48, 48, 51, 0.5);
                    --bento-item-hover: rgba(58, 58, 60, 0.9);
                }
            }
            
            .jump-bento-container {
                font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Segoe UI', Roboto, sans-serif;
                position: fixed;
                top: 20%;
                right: 0;
                z-index: 9999;
                transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1);
            }
            
            .jump-bento-container.collapsed {
                transform: translateX(calc(100% - 56px));
            }
            
            .jump-bento-container.collapsed .jump-bento-card {
                border-radius: 28px;
                min-width: 56px;
                max-width: 56px;
                box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
            }
            
            .jump-bento-container.collapsed .jump-bento-header {
                padding: 0;
                border: none;
                background: transparent;
                justify-content: center;
            }
            
            .jump-bento-container.collapsed .jump-bento-title-wrapper {
                display: none;
            }
            
            .jump-bento-container.collapsed .jump-bento-content {
                display: none;
            }
            
            .jump-bento-container.collapsed .jump-bento-empty {
                display: none;
            }
            
            .jump-bento-container.collapsed .jump-bento-toggle {
                margin: 12px;
                width: 32px;
                height: 32px;
            }
            
            .jump-bento-card {
                min-width: 280px;
                max-width: 320px;
                background: var(--bento-bg);
                backdrop-filter: blur(20px) saturate(180%);
                -webkit-backdrop-filter: blur(20px) saturate(180%);
                border: 1px solid var(--bento-border);
                border-radius: 20px;
                box-shadow: var(--bento-shadow);
                color: var(--bento-text);
                margin-right: 20px;
                overflow: hidden;
                transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
            }
            
            .jump-bento-header {
                padding: 16px 20px;
                border-bottom: 1px solid var(--bento-border);
                display: flex;
                justify-content: space-between;
                align-items: center;
                background: linear-gradient(135deg, var(--bento-item-bg) 0%, transparent 100%);
                transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
            }
            
            .jump-bento-title-wrapper {
                display: flex;
                align-items: center;
                transition: opacity 0.3s ease;
            }
            
            .jump-bento-title {
                font-size: 15px;
                font-weight: 600;
                letter-spacing: -0.01em;
                color: var(--bento-text);
                margin: 0;
            }
            
            .jump-bento-count {
                display: inline-flex;
                align-items: center;
                justify-content: center;
                min-width: 22px;
                height: 22px;
                padding: 0 6px;
                background: var(--bento-accent);
                color: white;
                border-radius: 11px;
                font-size: 12px;
                font-weight: 600;
                margin-left: 8px;
            }
            
            .jump-bento-toggle {
                width: 32px;
                height: 32px;
                border-radius: 50%;
                background: var(--bento-item-bg);
                border: none;
                display: flex;
                align-items: center;
                justify-content: center;
                cursor: pointer;
                transition: all 0.2s ease;
                color: var(--bento-text);
            }
            
            .jump-bento-toggle:hover {
                background: var(--bento-item-hover);
                transform: scale(1.05);
            }
            
            .jump-bento-toggle:active {
                transform: scale(0.95);
            }
            
            .jump-bento-nav {
                display: flex;
                gap: 8px;
                padding: 12px;
                border-bottom: 1px solid var(--bento-border);
                background: var(--bento-item-bg);
            }
            
            .jump-bento-nav-btn {
                flex: 1;
                padding: 8px 12px;
                background: var(--bento-item-bg);
                border: 1px solid var(--bento-border);
                border-radius: 8px;
                color: var(--bento-text);
                font-size: 12px;
                font-weight: 500;
                cursor: pointer;
                transition: all 0.2s ease;
                display: flex;
                align-items: center;
                justify-content: center;
                gap: 4px;
            }
            
            .jump-bento-nav-btn:hover {
                background: var(--bento-item-hover);
                transform: translateY(-1px);
                box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
            }
            
            .jump-bento-nav-btn:active {
                transform: translateY(0);
            }
            
            .jump-bento-nav-btn:disabled {
                opacity: 0.4;
                cursor: not-allowed;
                transform: none;
            }
            
            .jump-bento-nav-btn:disabled:hover {
                background: var(--bento-item-bg);
                box-shadow: none;
            }
            
            .jump-bento-kbd {
                display: inline-block;
                padding: 2px 6px;
                background: var(--bento-border);
                border-radius: 4px;
                font-size: 11px;
                font-family: monospace;
                margin-left: 4px;
            }
            
            .jump-bento-content {
                max-height: 60vh;
                overflow-y: auto;
                padding: 12px;
                scrollbar-width: thin;
                scrollbar-color: var(--bento-text-secondary) transparent;
            }
            
            .jump-bento-content::-webkit-scrollbar {
                width: 6px;
            }
            
            .jump-bento-content::-webkit-scrollbar-track {
                background: transparent;
            }
            
            .jump-bento-content::-webkit-scrollbar-thumb {
                background: var(--bento-text-secondary);
                border-radius: 3px;
            }
            
            .jump-bento-item {
                position: relative;
                display: flex;
                align-items: center;
                padding: 12px 16px;
                margin-bottom: 8px;
                background: var(--bento-item-bg);
                backdrop-filter: blur(10px);
                border-radius: 12px;
                transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
                text-decoration: none;
                color: var(--bento-text);
                border: 1px solid transparent;
                overflow: hidden;
            }
            
            .jump-bento-item::before {
                content: '';
                position: absolute;
                top: 0;
                left: 0;
                width: 3px;
                height: 100%;
                background: var(--bento-accent);
                transform: scaleY(0);
                transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
            }
            
            .jump-bento-item:hover {
                background: var(--bento-item-hover);
                border-color: var(--bento-border);
                transform: translateX(-4px);
                box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
            }
            
            .jump-bento-item:hover::before {
                transform: scaleY(1);
            }
            
            .jump-bento-item:active {
                transform: translateX(-2px) scale(0.98);
            }
            
            .jump-bento-icon {
                width: 32px;
                height: 32px;
                background: linear-gradient(135deg, var(--bento-accent) 0%, var(--bento-accent-hover) 100%);
                border-radius: 8px;
                display: flex;
                align-items: center;
                justify-content: center;
                margin-right: 12px;
                font-size: 14px;
                font-weight: 600;
                color: white;
                flex-shrink: 0;
            }
            
            .jump-bento-item-text {
                font-size: 13px;
                font-weight: 500;
                letter-spacing: -0.01em;
                flex: 1;
                overflow: hidden;
                text-overflow: ellipsis;
                white-space: nowrap;
            }
            
            .jump-bento-arrow {
                color: var(--bento-text-secondary);
                font-size: 16px;
                opacity: 0;
                transform: translateX(-8px);
                transition: all 0.3s ease;
            }
            
            .jump-bento-item:hover .jump-bento-arrow {
                opacity: 1;
                transform: translateX(0);
            }
            
            .jump-bento-empty {
                padding: 32px 20px;
                text-align: center;
            }
            
            .jump-bento-empty-icon {
                width: 64px;
                height: 64px;
                margin: 0 auto 16px;
                background: var(--bento-item-bg);
                border-radius: 50%;
                display: flex;
                align-items: center;
                justify-content: center;
                font-size: 32px;
                opacity: 0.5;
            }
            
            .jump-bento-empty-text {
                font-size: 14px;
                color: var(--bento-text-secondary);
                line-height: 1.5;
            }
            
            @keyframes fadeInUp {
                from {
                    opacity: 0;
                    transform: translateY(10px);
                }
                to {
                    opacity: 1;
                    transform: translateY(0);
                }
            }
            
            .jump-bento-item {
                animation: fadeInUp 0.4s ease backwards;
            }
            
            .jump-bento-item:nth-child(1) { animation-delay: 0.05s; }
            .jump-bento-item:nth-child(2) { animation-delay: 0.1s; }
            .jump-bento-item:nth-child(3) { animation-delay: 0.15s; }
            .jump-bento-item:nth-child(4) { animation-delay: 0.2s; }
            .jump-bento-item:nth-child(5) { animation-delay: 0.25s; }
            .jump-bento-item:nth-child(n+6) { animation-delay: 0.3s; }
        </style>
    `;

    // Generate content based on whether there are posts
    let contentHtml;
    let navHtml = '';
    
    if (isEmpty) {
        contentHtml = `
            <div class="jump-bento-empty">
                <div class="jump-bento-empty-icon">💭</div>
                <div class="jump-bento-empty-text">
                    还没有任何"贴贴标记"<br>
                    <span style="font-size: 12px; opacity: 0.7;">去给感兴趣的回复点个贴贴吧</span>
                </div>
            </div>
        `;
    } else {
        // 添加导航按钮
        navHtml = `
            <div class="jump-bento-nav">
                <button class="jump-bento-nav-btn" id="jumpBentoPrev" title="上一个 (P)">
                    ← 上一个<span class="jump-bento-kbd">P</span>
                </button>
                <button class="jump-bento-nav-btn" id="jumpBentoNext" title="下一个 (N)">
                    下一个 →<span class="jump-bento-kbd">N</span>
                </button>
            </div>
        `;
        
        const linksHtml = postIds.map((postId, index) => `
            <a href="#${postId}" class="jump-bento-item" data-post-id="${postId}" data-index="${index}">
                <div class="jump-bento-icon">#${index + 1}</div>
                <span class="jump-bento-item-text">${postId}</span>
                <span class="jump-bento-arrow">→</span>
            </a>
        `).join('');
        
        contentHtml = `<div class="jump-bento-content">${linksHtml}</div>`;
    }

    // Complete component HTML
    const componentHtml = `
        <div class="jump-bento-container" id="jumpBentoContainer">
            <div class="jump-bento-card">
                <div class="jump-bento-header">
                    <div class="jump-bento-title-wrapper">
                        <span class="jump-bento-title">贴贴回复</span>
                        ${!isEmpty ? `<span class="jump-bento-count">${postIds.length}</span>` : ''}
                    </div>
                    <button class="jump-bento-toggle" id="jumpBentoToggle" title="收起/展开">
                        <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
                            <path d="M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z"/>
                        </svg>
                    </button>
                </div>
                ${navHtml}
                ${contentHtml}
            </div>
        </div>
    `;
    
    return styles + componentHtml;
}

// Initialize component interactivity
function initializeComponent(postIds) {
    const container = document.getElementById('jumpBentoContainer');
    const toggleBtn = document.getElementById('jumpBentoToggle');
    
    if (!container || !toggleBtn) return;
    
    let currentIndex = -1; // 当前高亮的索引
    
    // Load saved state
    const isCollapsed = localStorage.getItem('jumpBentoCollapsed') === 'true';
    if (isCollapsed) {
        container.classList.add('collapsed');
        toggleBtn.innerHTML = `
            <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
                <path d="M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z"/>
            </svg>
        `;
    }
    
    // Toggle collapse/expand
    toggleBtn.addEventListener('click', () => {
        const collapsed = container.classList.toggle('collapsed');
        localStorage.setItem('jumpBentoCollapsed', collapsed);
        
        // Animate icon rotation and change
        toggleBtn.style.transform = 'rotate(180deg)';
        setTimeout(() => {
            toggleBtn.innerHTML = collapsed ? `
                <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
                    <path d="M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z"/>
                </svg>
            ` : `
                <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
                    <path d="M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z"/>
                </svg>
            `;
            toggleBtn.style.transform = '';
        }, 200);
    });
    
    if (postIds.length === 0) return; // 空状态不需要导航功能
    
    // 跳转到指定帖子的函数
    function jumpToPost(index) {
        if (index < 0 || index >= postIds.length) return;
        
        currentIndex = index;
        const postId = postIds[index];
        const targetElement = document.getElementById(postId);
        
        if (targetElement) {
            targetElement.scrollIntoView({ 
                behavior: 'smooth', 
                block: 'center' 
            });
            
            // Highlight effect
            targetElement.style.transition = 'background-color 0.6s ease';
            const originalBg = targetElement.style.backgroundColor;
            targetElement.style.backgroundColor = 'rgba(0, 113, 227, 0.1)';
            setTimeout(() => {
                targetElement.style.backgroundColor = originalBg;
            }, 1500);
            
            // 更新列表中的活跃状态
            updateActiveItem(index);
            updateNavButtons();
        }
    }
    
    // 更新列表中的活跃项
    function updateActiveItem(index) {
        const items = document.querySelectorAll('.jump-bento-item');
        items.forEach((item, i) => {
            if (i === index) {
                item.style.background = 'var(--bento-accent)';
                item.style.color = 'white';
                item.querySelector('.jump-bento-icon').style.background = 'rgba(255, 255, 255, 0.3)';
            } else {
                item.style.background = '';
                item.style.color = '';
                item.querySelector('.jump-bento-icon').style.background = '';
            }
        });
    }
    
    // 更新导航按钮状态
    function updateNavButtons() {
        const prevBtn = document.getElementById('jumpBentoPrev');
        const nextBtn = document.getElementById('jumpBentoNext');
        
        if (prevBtn && nextBtn) {
            prevBtn.disabled = currentIndex <= 0;
            nextBtn.disabled = currentIndex >= postIds.length - 1;
        }
    }
    
    // 上一个按钮
    const prevBtn = document.getElementById('jumpBentoPrev');
    if (prevBtn) {
        prevBtn.addEventListener('click', () => {
            if (currentIndex <= 0) {
                currentIndex = 0;
            } else {
                jumpToPost(currentIndex - 1);
            }
        });
    }
    
    // 下一个按钮
    const nextBtn = document.getElementById('jumpBentoNext');
    if (nextBtn) {
        nextBtn.addEventListener('click', () => {
            if (currentIndex < 0) {
                jumpToPost(0);
            } else if (currentIndex < postIds.length - 1) {
                jumpToPost(currentIndex + 1);
            }
        });
    }
    
    // Add smooth scroll behavior to links
    const links = document.querySelectorAll('.jump-bento-item');
    links.forEach((link, index) => {
        link.addEventListener('click', (e) => {
            e.preventDefault();
            jumpToPost(index);
        });
    });
    
    // 键盘快捷键监听
    document.addEventListener('keydown', (e) => {
        // 检查是否在输入框中
        const activeElement = document.activeElement;
        const isInputFocused = activeElement && (
            activeElement.tagName === 'INPUT' || 
            activeElement.tagName === 'TEXTAREA' || 
            activeElement.isContentEditable
        );
        
        // 如果在输入框中,不触发快捷键
        if (isInputFocused) return;
        
        // N - 下一个
        if (e.key === 'n' || e.key === 'N') {
            e.preventDefault();
            if (currentIndex < 0) {
                jumpToPost(0);
            } else if (currentIndex < postIds.length - 1) {
                jumpToPost(currentIndex + 1);
            }
        }
        
        // P - 上一个
        if (e.key === 'p' || e.key === 'P') {
            e.preventDefault();
            if (currentIndex <= 0) {
                currentIndex = 0;
            } else {
                jumpToPost(currentIndex - 1);
            }
        }
    });
    
    // 初始化按钮状态
    updateNavButtons();
}