Bangumi User Hover Panel-change

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

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