Bangumi User Hover Panel

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

  1. // ==UserScript==
  2. // @name Bangumi User Hover Panel
  3. // @name:zh-CN Bangumi 用户悬浮面板
  4. // @namespace https://github.com/CryoVit/jioben/tree/master/bangumi/
  5. // @version 0.6.5
  6. // @description fork of https://bgm.tv/dev/app/953. Display a hover panel when mouse hover on user link.
  7. // @description:zh-CN https://bgm.tv/dev/app/953 的修改版,鼠标悬浮在用户链接上方时出现悬浮框
  8. // @author cureDovahkiin + CryoVit
  9. // @match https://bangumi.tv/*
  10. // @match https://bgm.tv/*
  11. // @match https://chii.in/*
  12. // @icon https://bgm.tv/img/favicon.ico
  13. // @grant none
  14. // @license MIT
  15. // ==/UserScript==
  16.  
  17. (function () {
  18. /*
  19. 2 = timeline
  20. 4 = stats
  21. 8 = sinkuro
  22. 16 = anime
  23. 32 = game
  24. 64 = book
  25. 128 = [reserved] for music
  26. 256 = [reserved] for real
  27. the value is the sum of the entries to show,
  28. e.g. 28 = 4 + 8 + 16, means show stats, sinkuro and anime
  29. */
  30. if (localStorage.getItem('hover-panel-config') === null) { // default config
  31. localStorage.setItem('hover-panel-config', '28'); // 4 + 8 + 16
  32. }
  33. const entryStates = [
  34. ['在看', '看过', '想看', '搁置', '抛弃'],
  35. ['在玩', '玩过', '想玩', '搁置', '抛弃'],
  36. ['在读', '读过', '想读', '搁置', '抛弃']
  37. ];
  38. const cfgNames = ['时间线', '统计', '同步率', '动画', '游戏', '书籍'];
  39. const cfgTimeline = 2;
  40. const cfgStats = 4;
  41. const cfgSinkuro = 8;
  42. const cfgAnime = 16;
  43. let locker = false
  44. $('[href*="/user/"],#pm_sidebar a[onclick^="AddMSG"]').each(function () {
  45. let timer = null
  46. $(this).hover(function () {
  47. timer = setTimeout(() => {
  48. if (locker) return false
  49. if (this.text == "查看好友列表" || $(this).find('.avatarSize75').length > 0) return false
  50. locker = true
  51. const layout = document.createElement('div')
  52. let timer = null
  53. $(layout).addClass('user-hover')
  54. if ($(this).hasClass('avatar')) {
  55. $(layout).addClass('fix-avatar-hover')
  56. }
  57. if (document.body.clientWidth - this.getBoundingClientRect().right < 430) {
  58. $(layout).addClass('fix-right-hover')
  59. }
  60. layout.innerHTML = `<div class="lds-roller"><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div></div>`
  61. const userData = {}
  62. if (this.onclick) {
  63. userData.id = this.onclick.toString().split("'")[1]
  64. } else {
  65. let urlSplit = /.*\/user\/([^\/]*)\/?(.*)/.exec(this.href)
  66. if (urlSplit[2]) return
  67. userData.id = urlSplit[1]
  68. }
  69. userData.href = '/user/' + userData.id
  70. const req = {
  71. req1: null,
  72. req2: null
  73. }
  74. Promise.all([
  75. new Promise((r, j) => {
  76. req.req1 = $.ajax({
  77. url: userData.href,
  78. dataType: 'text',
  79. success: e => {
  80. userData.self = /<a class="avatar" href="([^"]*)">/.exec(e)[1].split('/').pop()
  81. if (userData.self != userData.id) {
  82. userData.sinkuro = /mall class="hot">\/([^<]*)<\/small>/.exec(e)[1]
  83. userData.sinkuroritsu = /<span class="percent" style="width:([^"]*)">/.exec(e)[1]
  84. userData.addFriend = /<a href="([^"']*)" id="connectFrd" class="chiiBtn">/.exec(e)
  85. userData.addFriend = userData.addFriend ? userData.addFriend[1] : false
  86. }
  87. userData.joinDate = /Bangumi<\/span> <span class="tip">([^<]*)<\/span>/.exec(e)[1]
  88. // userData.lastEvent = /<small class="time">([^<]*)<\/small><\/li>/.exec(e)
  89. userData.entry = [
  90. 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('部')),
  91. 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('部')),
  92. 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('本'))
  93. ]
  94. userData.stats = /<div class="gridStats">([\s\S]*)<\/div>/.exec(e)[1]
  95. userData.stats = Array.from(userData.stats.match(/<div[^>]*>([\s\S]*?)<\/div>/g).slice(0, 6), el => /<div[^>]*>([\s\S]*?)<\/div>/.exec(el)[1])
  96. userData.stats = userData.stats.map(el => Array.from(el.match(/<span[^>]*>([\s\S]*?)<\/span>/g), el => /<span[^>]*>([\s\S]*?)<\/span>/.exec(el)[1]))
  97. userData.timeline = /<ul class="timeline">([\s\S]*?)<\/ul>/.exec(e)[1]
  98. // console.log(userData)
  99. r()
  100. },
  101. error: () => {
  102. j()
  103. }
  104. })
  105. }),
  106. new Promise((r, j) => {
  107. req.req2 = $.ajax({
  108. url: 'https://api.bgm.tv/user/' + userData.id,
  109. dataType: 'json',
  110. success: e => {
  111. userData.name = e.nickname
  112. userData.avatar = e.avatar.large.replace(/https?/, 'https')
  113. userData.sign = e.sign
  114. userData.url = e.url
  115. userData.message = `https://bgm.tv/pm/compose/${e.id}.chii`
  116. r()
  117. },
  118. error: () => {
  119. j()
  120. }
  121. })
  122. })
  123. ]).then(() => {
  124. layout.innerHTML = `
  125. <img class='avater' src="${userData.avatar}"/>
  126. <div class='user-info'>
  127. <p class='user-name'><a href="${userData.href}" target="_blank">${userData.name}</a></p>
  128. <p class='user-joindate'>${userData.joinDate}</p><span class='user-id'>@${userData.id}</span>
  129. <p class='user-sign'>${userData.sign}</p>
  130. </div>
  131. ${
  132. ((localStorage.getItem('hover-panel-config') & cfgSinkuro) && userData.sinkuro) ? `
  133. <div class="shinkuro">
  134. <div style="width:${userData.sinkuroritsu}" class="shinkuroritsu"></div>
  135. <div class="shinkuro-text">
  136. <span>${userData.sinkuro}</span>
  137. <span>同步率:${userData.sinkuroritsu}</span>
  138. </div>
  139. </div>
  140. `: ''
  141. }
  142. <div class='user-stats'>
  143. ${(function () {
  144. const cfg = localStorage.getItem('hover-panel-config')
  145. let html = ''
  146. let odd = true
  147. for (let i = 0; i < 3; i++) {
  148. if (cfg & (cfgAnime << i)) {
  149. html += '<div class="stats-' + (odd ? 'odd' : 'even') + '">'
  150. let dt_j = 0
  151. for (let st_j = 0; st_j < 5; st_j++) {
  152. if (dt_j >= userData.entry[i].length || userData.entry[i][dt_j][1] != entryStates[i][st_j]) {
  153. html += `<span class="stats-zero">${entryStates[i][st_j]} <strong>0</strong></span>`
  154. } else {
  155. html += `<span>${entryStates[i][st_j]} <strong>${userData.entry[i][dt_j][0]}</strong></span>`
  156. dt_j++
  157. }
  158. }
  159. html += '</div>'
  160. odd = !odd
  161. }
  162. }
  163. if (cfg & cfgStats) {
  164. html += '<div class="stats-' + (odd ? 'odd' : 'even') + '">'
  165. for (let i = 0; i < 6; i++) {
  166. if (i == 2) {
  167. continue
  168. }
  169. if (userData.stats[i][0] == 0) { // '0.00' == 0
  170. html += `<span class="stats-zero">${userData.stats[i][1]} <strong>${userData.stats[i][0]}</strong></span>`
  171. } else {
  172. html += `<span>${userData.stats[i][1]} <strong>${userData.stats[i][0]}</strong></span>`
  173. }
  174. }
  175. html += '</div>'
  176. odd = !odd
  177. }
  178. return html
  179. })()}
  180. </div>
  181. ${
  182. (localStorage.getItem('hover-panel-config') & cfgTimeline) ? `
  183. <ul class="timeline" id="panel-timeline">${userData.timeline}</ul>
  184. `: ''
  185. }
  186. <!-- <span class='user-lastevent'>Last @ ${userData.lastEvent ? userData.lastEvent[1] : ''}</span> -->
  187. <a class = 'hover-panel-btn' href="${userData.message}" target="_blank">发送短信</a>
  188. <span id="panel-friend">
  189. ${ userData.addFriend ? `
  190. <a class='hover-panel-btn' href="${userData.addFriend}" id='PanelconnectFrd' href="javascript:void(0)">添加好友</a>
  191. `: `
  192. ${ userData.id == userData.self ? '' : `<span class = 'my-friend' >我的好友</span>`}
  193. `}
  194. </span>
  195. `
  196. let cb = document.createElement('a')
  197. cb.className = 'hover-panel-btn'
  198. cb.id = 'cfg-btn'
  199. cb.href = 'javascript:void(0)'
  200. cb.onclick = function () {
  201. let cfg = localStorage.getItem('hover-panel-config')
  202. let sub = document.createElement('div')
  203. sub.className = 'user-hover'
  204. sub.id = 'hover-panel-sub'
  205. sub.innerHTML = `
  206. <fieldset>
  207. <legend>设置显示项目</legend>
  208. ${(function () {
  209. let html = ''
  210. for (let i = 0; i < 6; i++) {
  211. html += `<div class='hover-cfg-item'>
  212. <input type='checkbox' id='hover-cfg-${i}' ${cfg & (2 << i) ? 'checked' : ''}>
  213. <label for='hover-cfg-${i}'>${cfgNames[i]}</label>
  214. </div>`
  215. }
  216. return html
  217. })()}
  218. </div>
  219. </fieldset>
  220. `
  221.  
  222. let cancel = document.createElement('a')
  223. cancel.className = 'hover-panel-btn'
  224. cancel.id = 'cfg-cancel-btn'
  225. cancel.href = 'javascript:void(0)'
  226. cancel.innerText = '取消'
  227. cancel.onclick = function () {
  228. $('#hover-panel-sub').remove()
  229. }
  230. sub.appendChild(cancel)
  231.  
  232. let save = document.createElement('a')
  233. save.className = 'hover-panel-btn'
  234. save.id = 'cfg-save-btn'
  235. save.href = 'javascript:void(0)'
  236. save.innerText = '保存'
  237. save.onclick = function () {
  238. let cfg = 0
  239. for (let i = 0; i < 6; i++) {
  240. if (document.getElementById(`hover-cfg-${i}`).checked) {
  241. cfg |= (2 << i)
  242. }
  243. }
  244. localStorage.setItem('hover-panel-config', cfg)
  245. $('#hover-panel-sub').remove()
  246. }
  247. sub.appendChild(save)
  248. document.body.appendChild(sub)
  249. }
  250. cb.innerText = '设置'
  251. layout.appendChild(cb)
  252.  
  253. $(layout).addClass('dataready')
  254. $('#PanelconnectFrd').click(function () {
  255. $('#panel-friend').html(`<span class='my-friend'>正在添加</span>`)
  256. $("#robot").fadeIn(500)
  257. $("#robot_balloon").html(AJAXtip['wait'] + AJAXtip['addingFrd'])
  258. $.ajax({
  259. type: "GET",
  260. url: this + '&ajax=1',
  261. success: function (html) {
  262. $('#PanelconnectFrd').hide()
  263. $('#panel-friend').html(`<span class = 'my-friend' >我的好友</span>`)
  264. $("#robot_balloon").html(AJAXtip['addFrd'])
  265. $("#robot").animate({
  266. opacity: 1
  267. }, 1000).fadeOut(500)
  268. localStorage.removeItem('bgmFriends')
  269. },
  270. error: function (html) {
  271. $("#robot_balloon").html(AJAXtip['error'])
  272. $("#robot").animate({
  273. opacity: 1
  274. }, 1000).fadeOut(500)
  275. $('#panel-friend').html(`<span class='my-friend-fail'>添加失败</span>`)
  276. }
  277. })
  278. return false
  279. })
  280. }).catch(() => {
  281. layout.innerHTML = `
  282. <p style='font-size:16px; margin:25px 30px'>
  283. <img style="height:15px;width:16px" src='/img/smiles/tv/15.gif'/><br/>
  284. 请求失败,请稍后再试。<br/><br/>或者使用<a href='https://bgm.tv'>bgm.tv</a>域名,</p>`
  285. $(layout).addClass('dataready')
  286. })
  287. function removeLayout () {
  288. setTimeout(() => {
  289. $(layout).remove()
  290. locker = false
  291. req.req1.abort()
  292. req.req2.abort()
  293. }, 200);
  294. }
  295. $(this).after(layout).mouseout(function () {
  296. timer = setTimeout(() => {
  297. removeLayout()
  298. }, 500);
  299. })
  300. $(layout).hover(function () {
  301. clearTimeout(timer)
  302. }, function () {
  303. removeLayout()
  304. })
  305. return false
  306. }, 500)
  307. },
  308. function () {
  309. clearTimeout(timer)
  310. }
  311. )
  312. })
  313.  
  314. // prevent user link at (1) page header (2) footer dock (3) reply form (4) timeline
  315. // from triggering hover panel
  316. $("#headerNeue2, #dock, #reply_wrapper, .tml_item").find("a[href*='/user/']").unbind();
  317.  
  318. const style = document.createElement("style");
  319. const heads = document.getElementsByTagName("head");
  320. style.setAttribute("type", "text/css");
  321. style.innerHTML = `
  322. :root {
  323. --bg-color: #fff;
  324. --text-color: #010101;
  325. --bg-pink: #fce9e9;
  326. --bg-sky: #c2e1fc;
  327. --box-shadow: #ddd;
  328. --text-gray: #6e6e6e;
  329. --bg-filter: blur(10px) contrast(90%);
  330. }
  331. [data-theme='dark'] {
  332. --bg-color: #2d2e2f;
  333. --text-color: #f7f7f7;
  334. --bg-pink: #3c3c3c;
  335. --bg-sky: #3c3c3c;
  336. --box-shadow: #6e6e6e;
  337. --text-gray: #aaa;
  338. --bg-filter: blur(10px) contrast(50%);
  339. }
  340. .user-hover {
  341. position: absolute;
  342. width: 430px;
  343. /* background: var(--bg-color); */
  344. box-shadow: 0px 0px 4px 1px var(--box-shadow);
  345. transition: all .2s ease-in;
  346. transform: translate(0,6 px);
  347. font-size: 12px;
  348. z-index:999;
  349. color: var(--text-color);
  350. line-height: 130%;
  351. border-radius: 15px;
  352. -webkit-border-radius: 15px;
  353. backdrop-filter: var(--bg-filter);
  354. -webkit-backdrop-filter: var(--bg-filter);
  355. }
  356. .fix-avatar-hover {
  357. transform: translate(55px, 20px)
  358. }
  359. .fix-right-hover {
  360. transform: translate(-430px, 6px)
  361. }
  362. .fix-avatar-hover.fix-right-hover {
  363. transform: translate(-485px, 20px)
  364. }
  365.  
  366. /* basic info */
  367. div.dataready {
  368. padding: 8px;
  369. font-weight: normal;
  370. text-align: left;
  371. }
  372. /* span.user-lastevent {
  373. margin-top: 3px;
  374. display: inline-block;
  375. vertical-align: top;
  376. color: var(--text-gray);
  377. } */
  378. div.dataready img {
  379. height: 75px;
  380. width:75px;
  381. border-radius: 5px;
  382. }
  383. .user-info {
  384. display: inline-block;
  385. vertical-align: top;
  386. max-width: 250px;
  387. margin: 0 0 10px 10px;
  388. }
  389. .user-info .user-name {
  390. font-size: 20px;
  391. font-weight: bold;
  392. }
  393. .user-info .user-joindate {
  394. background-color: #f09199;
  395. display: inline-block;
  396. color: #f7f7f7;
  397. border-radius: 10px;
  398. padding: 0 10px;
  399. margin: 8px 4px 3px 0;
  400. }
  401. .user-info .user-id{
  402. font-size: 12px;
  403. font-weight:normal;
  404. color: var(--text-gray);
  405. }
  406. .user-info .user-sign {
  407. word-break: break-all;
  408. margin-top: 3px;
  409. color: var(--text-gray);
  410. }
  411.  
  412. /* stats */
  413. .user-stats {
  414. padding: 10px 0px 5px;
  415. margin-bottom: 0;
  416. }
  417. .user-stats span {
  418. display: inline-block;
  419. padding: 4px;
  420. width: 19%;
  421. box-sizing: border-box;
  422. border-left: 4px solid #f09199;
  423. background-color: var(--bg-pink);
  424. color: var(--text-color);
  425. margin: 0 1% 1% 0;
  426. }
  427. .stats-even span {
  428. border-left: 4px solid #369cf8;
  429. background-color: var(--bg-sky);
  430. }
  431. .stats-zero {
  432. opacity: 0.5;
  433. }
  434.  
  435. /* shinkuro */
  436. .shinkuro {
  437. width: 100%;
  438. height: 20px;
  439. background-color: var(--bg-sky);
  440. line-height: 20px;
  441. border-radius: 10px;
  442. margin-top: 5px;
  443. }
  444. .shinkuro-text {
  445. position: absolute;
  446. width: 100%;
  447. height: 20px;
  448. display: flex;
  449. align-items: center;
  450. justify-content: space-between;
  451. }
  452. .shinkuro-text span {
  453. color: var(--text-color);
  454. }
  455. .shinkuroritsu {
  456. height: 20px;
  457. float: left;
  458. border-radius: 10px;
  459. background: #369cf8;
  460. }
  461. .shinkuro-text span:nth-of-type(1) {
  462. margin-left: 10px;
  463. }
  464. .shinkuro-text span:nth-of-type(2) {
  465. margin-right: 26px;
  466. }
  467.  
  468. /* timeline */
  469. #panel-timeline li {
  470. margin-top: 0;
  471. }
  472. #panel-timeline a {
  473. display: inline !important;
  474. }
  475. #panel-timeline .time {
  476. color: var(--text-gray);
  477. }
  478.  
  479. /* buttons */
  480. a.hover-panel-btn, span.my-friend, span.my-friend-fail {
  481. display: inline-block;
  482. float: right;
  483. color: white;
  484. padding: 1px 8px;
  485. border-radius: 10px;
  486. margin: 8px 0 0 10px;
  487. transition: all .2s ease-in;
  488. }
  489. a.hover-panel-btn {
  490. background: #f09199;
  491. transition: all .2s ease-in;
  492. }
  493. span.my-friend {
  494. background: #6eb76e;
  495. }
  496. span.my-friend-fail {
  497. background: red;
  498. }
  499. #cfg-btn {
  500. background: #369cf8;
  501. float: left;
  502. margin-left: 0;
  503. }
  504.  
  505. /* animation */
  506. .lds-roller {
  507. display: inline-block;
  508. position: relative;
  509. width: 64px;
  510. height: 64px;
  511. margin:10px 20px
  512. }
  513. .lds-roller div {
  514. animation: lds-roller 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
  515. transform-origin: 32px 32px;
  516. }
  517. .lds-roller div:after {
  518. content: " ";
  519. display: block;
  520. position: absolute;
  521. width: 6px;
  522. height: 6px;
  523. border-radius: 50%;
  524. background: #f09199;
  525. margin: -3px 0 0 -3px;
  526. }
  527. .lds-roller div:nth-child(1) {
  528. animation-delay: -0.036s;
  529. }
  530. .lds-roller div:nth-child(1):after {
  531. top: 50px;
  532. left: 50px;
  533. }
  534. .lds-roller div:nth-child(2) {
  535. animation-delay: -0.072s;
  536. }
  537. .lds-roller div:nth-child(2):after {
  538. top: 54px;
  539. left: 45px;
  540. }
  541. .lds-roller div:nth-child(3) {
  542. animation-delay: -0.108s;
  543. }
  544. .lds-roller div:nth-child(3):after {
  545. top: 57px;
  546. left: 39px;
  547. }
  548. .lds-roller div:nth-child(4) {
  549. animation-delay: -0.144s;
  550. }
  551. .lds-roller div:nth-child(4):after {
  552. top: 58px;
  553. left: 32px;
  554. }
  555. .lds-roller div:nth-child(5) {
  556. animation-delay: -0.18s;
  557. }
  558. .lds-roller div:nth-child(5):after {
  559. top: 57px;
  560. left: 25px;
  561. }
  562. .lds-roller div:nth-child(6) {
  563. animation-delay: -0.216s;
  564. }
  565. .lds-roller div:nth-child(6):after {
  566. top: 54px;
  567. left: 19px;
  568. }
  569. .lds-roller div:nth-child(7) {
  570. animation-delay: -0.252s;
  571. }
  572. .lds-roller div:nth-child(7):after {
  573. top: 50px;
  574. left: 14px;
  575. }
  576. .lds-roller div:nth-child(8) {
  577. animation-delay: -0.288s;
  578. }
  579. .lds-roller div:nth-child(8):after {
  580. top: 45px;
  581. left: 10px;
  582. }
  583. @keyframes lds-roller {
  584. 0% {
  585. transform: rotate(0deg);
  586. }
  587. 100% {
  588. transform: rotate(360deg);
  589. }
  590. }
  591. #comment_list div.sub_reply_collapse {
  592. opacity: 1;
  593. }
  594.  
  595. /* config panel */
  596. #hover-panel-sub {
  597. width: 150px;
  598. height: 160px;
  599. padding: 5px;
  600. line-height: 1.5;
  601. position: fixed;
  602. top: 50%;
  603. left: 50%;
  604. transform: translate(-50%, -50%);
  605. z-index: 1000;
  606. }
  607. #hover-panel-sub legend {
  608. font-size: 14px;
  609. font-weight: bold;
  610. text-align: center;
  611. }
  612. #hover-panel-sub fieldset {
  613. padding: 0 5px;
  614. }
  615. #hover-panel-sub .hover-panel-btn {
  616. display: inline-block;
  617. text-align: center;
  618. }
  619. #cfg-cancel-btn {
  620. position: absolute;
  621. left: 14px;
  622. bottom: 6px;
  623. background: #f09199;
  624. }
  625. #cfg-save-btn {
  626. position: absolute;
  627. right: 24px;
  628. bottom: 6px;
  629. background: #6eb76e;
  630. }
  631. `
  632. heads[0].append(style)
  633. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址