Bangumi User Hover Panel

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

目前為 2023-05-06 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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.3
// @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 () {
    /*
        config: what to show in the hover panel
        2 = timeline
        4 = stats
        8 = sinkuro
        16 = anime
        32 = game
        64 = book
        128 = [reserved] music
        256 = [reserved] real
        the value is the sum of the entries you want
        [reserved] means not implemented yet
    */
    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)

                        sub.style.position = 'fixed'
                        sub.style.top = '50%'
                        sub.style.left = '50%'
                        sub.style.transform = 'translate(-50%, -50%)'
                        sub.style.zIndex = 1000
                        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)
            }
        )
    })

    // user's own avatar & link at (1) page header (2) footer dock (3) reply form (4) timeline
    $("#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: 5px;
            -webkit-border-radius: 5px;
            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) !important;
            color: var(--text-color) !important;
            margin: 0 1% 1% 0;
        }
        .stats-even span {
            border-left: 4px solid #369cf8;
            background-color: var(--bg-sky) !important;
        }
        .stats-zero {
            opacity: 0.5;
        }

        /* shinkuro */
        .shinkuro {
            width: 100%;
            height: 20px;
            background-color: var(--bg-sky) !important;
            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) !important;
        }
        .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 {
            line-height: 100%;
        }
        #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;
            margin-bottom: 8px;
            color: white;
            padding: 1px 8px;
            border-radius: 10px;
            margin-left:10px;
            transition: all .2s ease-in;
        }
        a.hover-panel-btn {
            background: #f09199;
            transition: all .2s ease-in;
        }
        span.my-friend {
            background: #6eb76e;
            color: white !important;
        }
        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;
        }
        #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: 0;
            background: #f09199;
        }
        #cfg-save-btn {
            position: absolute;
            right: 24px;
            bottom: 0;
            background: #6eb76e;
        }
    `
    heads[0].append(style)
})();