- // ==UserScript==
- // @name Bangumi User Hover Panel-change
- // @name:zh-CN Bangumi 用户悬浮面板-改
- // @namespace https://github.com/stay206/bangumi/
- // @version 1.0.0
- // @description fork of https://bgm.tv/dev/app/2647. Display a hover panel when mouse hover on user link.
- // @description:zh-CN https://bgm.tv/dev/app/2647 的修改版,鼠标悬浮在用户链接上方时出现悬浮框,添加了wiki协助记录
- // @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`
- userData.wiki = `https://patch.bgm38.tv/contrib/${e.id}`
- 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>
- <a class = 'hover-panel-btn' href="${userData.wiki}" 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)
- })();