Bangumi User Hover Panel

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

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