Bangumi User Hover Panel

fork of https://bgm.tv/dev/app/953. Display a hover panel when mouse hover on user link.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

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