Greasy Fork

Greasy Fork is available in English.

Bangumi 用户悬浮面板

https://bgm.tv/dev/app/953 的修改版,鼠标悬浮在用户链接上方时出现悬浮框

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Bangumi User Hover Panel
// @name:zh-CN   Bangumi 用户悬浮面板
// @namespace    https://github.com/CryoVit/jioben/tree/master/bangumi/
// @version      0.6.5
// @description  fork of https://bgm.tv/dev/app/953. Display a hover panel when mouse hover on user link.
// @description:zh-CN  https://bgm.tv/dev/app/953 的修改版,鼠标悬浮在用户链接上方时出现悬浮框
// @author       cureDovahkiin + CryoVit
// @match        https://bangumi.tv/*
// @match        https://bgm.tv/*
// @match        https://chii.in/*
// @icon         https://bgm.tv/img/favicon.ico
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
    /*
        2 = timeline
        4 = stats
        8 = sinkuro
        16 = anime
        32 = game
        64 = book
        128 = [reserved] for music
        256 = [reserved] for real
        the value is the sum of the entries to show,
        e.g. 28 = 4 + 8 + 16, means show stats, sinkuro and anime
    */
    if (localStorage.getItem('hover-panel-config') === null) { // default config
        localStorage.setItem('hover-panel-config', '28'); // 4 + 8 + 16
    }
    const entryStates = [
        ['在看', '看过', '想看', '搁置', '抛弃'],
        ['在玩', '玩过', '想玩', '搁置', '抛弃'],
        ['在读', '读过', '想读', '搁置', '抛弃']
    ];
    const cfgNames = ['时间线', '统计', '同步率', '动画', '游戏', '书籍'];
    const cfgTimeline = 2;
    const cfgStats = 4;
    const cfgSinkuro = 8;
    const cfgAnime = 16;
    let locker = false
    $('[href*="/user/"],#pm_sidebar a[onclick^="AddMSG"]').each(function () {
        let timer = null
        $(this).hover(function () {
            timer = setTimeout(() => {
                if (locker) return false
                if (this.text == "查看好友列表" || $(this).find('.avatarSize75').length > 0) return false
                locker = true
                const layout = document.createElement('div')
                let timer = null
                $(layout).addClass('user-hover')
                if ($(this).hasClass('avatar')) {
                    $(layout).addClass('fix-avatar-hover')
                }
                if (document.body.clientWidth - this.getBoundingClientRect().right < 430) {
                    $(layout).addClass('fix-right-hover')
                }
                layout.innerHTML = `<div class="lds-roller"><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div></div>`
                const userData = {}
                if (this.onclick) {
                    userData.id = this.onclick.toString().split("'")[1]
                } else {
                    let urlSplit = /.*\/user\/([^\/]*)\/?(.*)/.exec(this.href)
                    if (urlSplit[2]) return
                    userData.id = urlSplit[1]
                }
                userData.href = '/user/' + userData.id
                const req = {
                    req1: null,
                    req2: null
                }
                Promise.all([
                    new Promise((r, j) => {
                        req.req1 = $.ajax({
                            url: userData.href,
                            dataType: 'text',
                            success: e => {
                                userData.self = /<a class="avatar" href="([^"]*)">/.exec(e)[1].split('/').pop()
                                if (userData.self != userData.id) {
                                    userData.sinkuro = /mall class="hot">\/([^<]*)<\/small>/.exec(e)[1]
                                    userData.sinkuroritsu = /<span class="percent" style="width:([^"]*)">/.exec(e)[1]
                                    userData.addFriend = /<a href="([^"']*)" id="connectFrd" class="chiiBtn">/.exec(e)
                                    userData.addFriend = userData.addFriend ? userData.addFriend[1] : false
                                }
                                userData.joinDate = /Bangumi<\/span> <span class="tip">([^<]*)<\/span>/.exec(e)[1]
                                // userData.lastEvent = /<small class="time">([^<]*)<\/small><\/li>/.exec(e)
                                userData.entry = [
                                    Array.from(e.match(/<a href="\/anime\/list[^>=]*>([0-9]{1,4}[^<]*)/g) || [], el => />([0-9]{1,5}.*)/.exec(el)[1]).map(el => el.split('部')),
                                    Array.from(e.match(/<a href="\/game\/list[^>=]*>([0-9]{1,4}[^<]*)/g) || [], el => />([0-9]{1,5}.*)/.exec(el)[1]).map(el => el.split('部')),
                                    Array.from(e.match(/<a href="\/book\/list[^>=]*>([0-9]{1,4}[^<]*)/g) || [], el => />([0-9]{1,5}.*)/.exec(el)[1]).map(el => el.split('本'))
                                ]
                                userData.stats = /<div class="gridStats">([\s\S]*)<\/div>/.exec(e)[1]
                                userData.stats = Array.from(userData.stats.match(/<div[^>]*>([\s\S]*?)<\/div>/g).slice(0, 6), el => /<div[^>]*>([\s\S]*?)<\/div>/.exec(el)[1])
                                userData.stats = userData.stats.map(el => Array.from(el.match(/<span[^>]*>([\s\S]*?)<\/span>/g), el => /<span[^>]*>([\s\S]*?)<\/span>/.exec(el)[1]))
                                userData.timeline = /<ul class="timeline">([\s\S]*?)<\/ul>/.exec(e)[1]
                                // console.log(userData)
                                r()
                            },
                            error: () => {
                                j()
                            }
                        })
                    }),
                    new Promise((r, j) => {
                        req.req2 = $.ajax({
                            url: 'https://api.bgm.tv/user/' + userData.id,
                            dataType: 'json',
                            success: e => {
                                userData.name = e.nickname
                                userData.avatar = e.avatar.large.replace(/https?/, 'https')
                                userData.sign = e.sign
                                userData.url = e.url
                                userData.message = `https://bgm.tv/pm/compose/${e.id}.chii`
                                r()
                            },
                            error: () => {
                                j()
                            }
                        })
                    })
                ]).then(() => {
                    layout.innerHTML = `
                        <img class='avater' src="${userData.avatar}"/>
                        <div class='user-info'>
                            <p class='user-name'><a href="${userData.href}" target="_blank">${userData.name}</a></p>
                            <p class='user-joindate'>${userData.joinDate}</p><span class='user-id'>@${userData.id}</span>
                            <p class='user-sign'>${userData.sign}</p>
                        </div>
                        ${
                        ((localStorage.getItem('hover-panel-config') & cfgSinkuro) && userData.sinkuro) ? `
                            <div class="shinkuro">
                            <div style="width:${userData.sinkuroritsu}" class="shinkuroritsu"></div>
                            <div class="shinkuro-text">
                                <span>${userData.sinkuro}</span> 
                                <span>同步率:${userData.sinkuroritsu}</span> 
                            </div>                                      
                            </div>
                            `: ''
                        }                
                        <div class='user-stats'>
                            ${(function () {
                                const cfg = localStorage.getItem('hover-panel-config')
                                let html = ''
                                let odd = true
                                for (let i = 0; i < 3; i++) {
                                    if (cfg & (cfgAnime << i)) {
                                        html += '<div class="stats-' + (odd ? 'odd' : 'even') + '">'
                                        let dt_j = 0
                                        for (let st_j = 0; st_j < 5; st_j++) {
                                            if (dt_j >= userData.entry[i].length || userData.entry[i][dt_j][1] != entryStates[i][st_j]) {
                                                html += `<span class="stats-zero">${entryStates[i][st_j]} <strong>0</strong></span>`
                                            } else {
                                                html += `<span>${entryStates[i][st_j]} <strong>${userData.entry[i][dt_j][0]}</strong></span>`
                                                dt_j++
                                            }
                                        }
                                        html += '</div>'
                                        odd = !odd
                                    }
                                }
                                if (cfg & cfgStats) {
                                    html += '<div class="stats-' + (odd ? 'odd' : 'even') + '">'
                                    for (let i = 0; i < 6; i++) {
                                        if (i == 2) {
                                            continue
                                        }
                                        if (userData.stats[i][0] == 0) { // '0.00' == 0
                                            html += `<span class="stats-zero">${userData.stats[i][1]} <strong>${userData.stats[i][0]}</strong></span>`
                                        } else {
                                            html += `<span>${userData.stats[i][1]} <strong>${userData.stats[i][0]}</strong></span>`
                                        }
                                    }
                                    html += '</div>'
                                    odd = !odd
                                }
                                return html
                            })()}
                        </div>
                        ${
                        (localStorage.getItem('hover-panel-config') & cfgTimeline) ? `
                            <ul class="timeline" id="panel-timeline">${userData.timeline}</ul>
                            `: ''
                        }
                        <!-- <span class='user-lastevent'>Last @ ${userData.lastEvent ? userData.lastEvent[1] : ''}</span> -->
                        <a class = 'hover-panel-btn' href="${userData.message}" target="_blank">发送短信</a>
                        <span id="panel-friend">
                        ${ userData.addFriend ? `
                                <a class='hover-panel-btn' href="${userData.addFriend}" id='PanelconnectFrd' href="javascript:void(0)">添加好友</a>                    
                            `: `
                        ${ userData.id == userData.self ? '' : `<span class = 'my-friend' >我的好友</span>`}
                            `}
                        </span>
                        `
                    
                    let cb = document.createElement('a')
                    cb.className = 'hover-panel-btn'
                    cb.id = 'cfg-btn'
                    cb.href = 'javascript:void(0)'
                    cb.onclick = function () {
                        let cfg = localStorage.getItem('hover-panel-config')
                        let sub = document.createElement('div')
                        sub.className = 'user-hover'
                        sub.id = 'hover-panel-sub'
                        sub.innerHTML = `
                            <fieldset>
                                <legend>设置显示项目</legend>
                                ${(function () {
                                    let html = ''
                                    for (let i = 0; i < 6; i++) {
                                        html += `<div class='hover-cfg-item'>
                                            <input type='checkbox' id='hover-cfg-${i}' ${cfg & (2 << i) ? 'checked' : ''}>
                                            <label for='hover-cfg-${i}'>${cfgNames[i]}</label>
                                        </div>`
                                    }
                                    return html
                                })()}
                                </div>
                            </fieldset>
                        `

                        let cancel = document.createElement('a')
                        cancel.className = 'hover-panel-btn'
                        cancel.id = 'cfg-cancel-btn'
                        cancel.href = 'javascript:void(0)'
                        cancel.innerText = '取消'
                        cancel.onclick = function () {
                            $('#hover-panel-sub').remove()
                        }
                        sub.appendChild(cancel)

                        let save = document.createElement('a')
                        save.className = 'hover-panel-btn'
                        save.id = 'cfg-save-btn'
                        save.href = 'javascript:void(0)'
                        save.innerText = '保存'
                        save.onclick = function () {
                            let cfg = 0
                            for (let i = 0; i < 6; i++) {
                                if (document.getElementById(`hover-cfg-${i}`).checked) {
                                    cfg |= (2 << i)
                                }
                            }
                            localStorage.setItem('hover-panel-config', cfg)
                            $('#hover-panel-sub').remove()
                        }
                        sub.appendChild(save)
                        document.body.appendChild(sub)
                    }
                    cb.innerText = '设置'
                    layout.appendChild(cb)

                    $(layout).addClass('dataready')
                    $('#PanelconnectFrd').click(function () {
                        $('#panel-friend').html(`<span class='my-friend'>正在添加</span>`)
                        $("#robot").fadeIn(500)
                        $("#robot_balloon").html(AJAXtip['wait'] + AJAXtip['addingFrd'])
                        $.ajax({
                            type: "GET",
                            url: this + '&ajax=1',
                            success: function (html) {
                                $('#PanelconnectFrd').hide()
                                $('#panel-friend').html(`<span class = 'my-friend' >我的好友</span>`)
                                $("#robot_balloon").html(AJAXtip['addFrd'])
                                $("#robot").animate({
                                    opacity: 1
                                }, 1000).fadeOut(500)
                                localStorage.removeItem('bgmFriends')
                            },
                            error: function (html) {
                                $("#robot_balloon").html(AJAXtip['error'])
                                $("#robot").animate({
                                    opacity: 1
                                }, 1000).fadeOut(500)
                                $('#panel-friend').html(`<span class='my-friend-fail'>添加失败</span>`)
                            }
                        })
                        return false
                    })
                }).catch(() => {
                    layout.innerHTML = `
                        <p style='font-size:16px; margin:25px 30px'>
                        <img style="height:15px;width:16px" src='/img/smiles/tv/15.gif'/><br/>
                        请求失败,请稍后再试。<br/><br/>或者使用<a href='https://bgm.tv'>bgm.tv</a>域名,</p>`
                    $(layout).addClass('dataready')
                })
                function removeLayout () {
                    setTimeout(() => {
                        $(layout).remove()
                        locker = false
                        req.req1.abort()
                        req.req2.abort()
                    }, 200);
                }
                $(this).after(layout).mouseout(function () {
                    timer = setTimeout(() => {
                        removeLayout()
                    }, 500);
                })
                $(layout).hover(function () {
                    clearTimeout(timer)
                }, function () {
                    removeLayout()
                })
                return false
            }, 500)
        },
            function () {
                clearTimeout(timer)
            }
        )
    })

    // prevent user link at (1) page header (2) footer dock (3) reply form (4) timeline
    // from triggering hover panel
    $("#headerNeue2, #dock, #reply_wrapper, .tml_item").find("a[href*='/user/']").unbind();

    const style = document.createElement("style");
    const heads = document.getElementsByTagName("head");
    style.setAttribute("type", "text/css");
    style.innerHTML = `
        :root {
            --bg-color: #fff;
            --text-color: #010101;
            --bg-pink: #fce9e9;
            --bg-sky: #c2e1fc;
            --box-shadow: #ddd;
            --text-gray: #6e6e6e;
            --bg-filter: blur(10px) contrast(90%);
        }
        [data-theme='dark'] {
            --bg-color: #2d2e2f;
            --text-color: #f7f7f7;
            --bg-pink: #3c3c3c;
            --bg-sky: #3c3c3c;
            --box-shadow: #6e6e6e;
            --text-gray: #aaa;
            --bg-filter: blur(10px) contrast(50%);
        }
        .user-hover {
            position: absolute;
            width: 430px;
            /* background: var(--bg-color); */
            box-shadow: 0px 0px 4px 1px var(--box-shadow);
            transition: all .2s ease-in;
            transform: translate(0,6 px);
            font-size: 12px;
            z-index:999;
            color: var(--text-color);
            line-height: 130%;
            border-radius: 15px;
            -webkit-border-radius: 15px;
            backdrop-filter: var(--bg-filter);
            -webkit-backdrop-filter: var(--bg-filter);
        }
        .fix-avatar-hover {
            transform: translate(55px, 20px)
        }
        .fix-right-hover {
            transform: translate(-430px, 6px)
        }
        .fix-avatar-hover.fix-right-hover {
            transform: translate(-485px, 20px)
        }

        /* basic info */
        div.dataready {
            padding: 8px;
            font-weight: normal;
            text-align: left;
        }
        /* span.user-lastevent {
            margin-top: 3px;
            display: inline-block;
            vertical-align: top;
            color: var(--text-gray);
        } */
        div.dataready img {
            height: 75px;
            width:75px;
            border-radius: 5px;
        }
        .user-info {
            display: inline-block;
            vertical-align: top;
            max-width: 250px;
            margin: 0 0 10px 10px;
        }
        .user-info .user-name {
            font-size: 20px;
            font-weight: bold;
        }
        .user-info .user-joindate {
            background-color: #f09199;
            display: inline-block;
            color: #f7f7f7;
            border-radius: 10px;
            padding: 0 10px;
            margin: 8px 4px 3px 0;
        }
        .user-info .user-id{
            font-size: 12px;
            font-weight:normal;
            color: var(--text-gray);
        }
        .user-info .user-sign {
            word-break: break-all;
            margin-top: 3px;
            color: var(--text-gray);
        }

        /* stats */
        .user-stats {
            padding: 10px 0px 5px;
            margin-bottom: 0;
        }
        .user-stats span {
            display: inline-block;
            padding: 4px;
            width: 19%;
            box-sizing: border-box;
            border-left: 4px solid #f09199;
            background-color: var(--bg-pink);
            color: var(--text-color);
            margin: 0 1% 1% 0;
        }
        .stats-even span {
            border-left: 4px solid #369cf8;
            background-color: var(--bg-sky);
        }
        .stats-zero {
            opacity: 0.5;
        }

        /* shinkuro */
        .shinkuro {
            width: 100%;
            height: 20px;
            background-color: var(--bg-sky);
            line-height: 20px;
            border-radius: 10px;
            margin-top: 5px;
        }
        .shinkuro-text {
            position: absolute;
            width: 100%;
            height: 20px;
            display: flex;
            align-items: center;
            justify-content: space-between;
        }
        .shinkuro-text span {
            color: var(--text-color);
        }
        .shinkuroritsu {
            height: 20px;
            float: left;
            border-radius: 10px;
            background: #369cf8;
        }
        .shinkuro-text span:nth-of-type(1) {
            margin-left: 10px;
        }
        .shinkuro-text span:nth-of-type(2) {
            margin-right: 26px;
        }

        /* timeline */
        #panel-timeline li {
            margin-top: 0;
        }
        #panel-timeline a {
            display: inline !important;
        }
        #panel-timeline .time {
            color: var(--text-gray);
        }

        /* buttons */
        a.hover-panel-btn, span.my-friend, span.my-friend-fail {
            display: inline-block;
            float: right;
            color: white;
            padding: 1px 8px;
            border-radius: 10px;
            margin: 8px 0 0 10px;
            transition: all .2s ease-in;
        }
        a.hover-panel-btn {
            background: #f09199;
            transition: all .2s ease-in;
        }
        span.my-friend {
            background: #6eb76e;
        }
        span.my-friend-fail {
            background: red;
        }
        #cfg-btn {
            background: #369cf8;
            float: left;
            margin-left: 0;
        }

        /* animation */
        .lds-roller {
            display: inline-block;
            position: relative;
            width: 64px;
            height: 64px;
            margin:10px 20px
        }
        .lds-roller div {
            animation: lds-roller 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
            transform-origin: 32px 32px;
        }
        .lds-roller div:after {
            content: " ";
            display: block;
            position: absolute;
            width: 6px;
            height: 6px;
            border-radius: 50%;
            background: #f09199;
            margin: -3px 0 0 -3px;
        }
        .lds-roller div:nth-child(1) {
            animation-delay: -0.036s;
        }
        .lds-roller div:nth-child(1):after {
            top: 50px;
            left: 50px;
        }
        .lds-roller div:nth-child(2) {
            animation-delay: -0.072s;
        }
        .lds-roller div:nth-child(2):after {
            top: 54px;
            left: 45px;
        }
        .lds-roller div:nth-child(3) {
            animation-delay: -0.108s;
        }
        .lds-roller div:nth-child(3):after {
            top: 57px;
            left: 39px;
        }
        .lds-roller div:nth-child(4) {
            animation-delay: -0.144s;
        }
        .lds-roller div:nth-child(4):after {
            top: 58px;
            left: 32px;
        }
        .lds-roller div:nth-child(5) {
            animation-delay: -0.18s;
        }
        .lds-roller div:nth-child(5):after {
            top: 57px;
            left: 25px;
        }
        .lds-roller div:nth-child(6) {
            animation-delay: -0.216s;
        }
        .lds-roller div:nth-child(6):after {
            top: 54px;
            left: 19px;
        }
        .lds-roller div:nth-child(7) {
            animation-delay: -0.252s;
        }
        .lds-roller div:nth-child(7):after {
            top: 50px;
            left: 14px;
        }
        .lds-roller div:nth-child(8) {
            animation-delay: -0.288s;
        }
        .lds-roller div:nth-child(8):after {
            top: 45px;
            left: 10px;
        }
        @keyframes lds-roller {
            0% {
                transform: rotate(0deg);
            }
            100% {
                transform: rotate(360deg);
            }
        }
        
        #comment_list div.sub_reply_collapse {
            opacity: 1;
        }

        /* config panel */
        #hover-panel-sub {
            width: 150px;
            height: 160px;
            padding: 5px;
            line-height: 1.5;
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            z-index: 1000;
        }
        #hover-panel-sub legend {
            font-size: 14px;
            font-weight: bold;
            text-align: center;
        }
        #hover-panel-sub fieldset {
            padding: 0 5px;
        }
        #hover-panel-sub .hover-panel-btn {
            display: inline-block;
            text-align: center;
        }
        #cfg-cancel-btn {
            position: absolute;
            left: 14px;
            bottom: 6px;
            background: #f09199;
        }
        #cfg-save-btn {
            position: absolute;
            right: 24px;
            bottom: 6px;
            background: #6eb76e;
        }
    `
    heads[0].append(style)
})();