Clean Bilibili

Cleanup moot UI widgets from bilibili.

  1. // ==UserScript==
  2. // @name Clean Bilibili
  3. // @namespace https://ntnyq.com
  4. // @version 0.0.8
  5. // @description Cleanup moot UI widgets from bilibili.
  6. // @author ntnyq (https://ntnyq.com)
  7. // @license MIT
  8. // @homepageURL https://github.com/ntnyq/clean-bilibili
  9. // @supportURL https://github.com/ntnyq/clean-bilibili
  10. // @match http*://*.bilibili.com/*
  11. // @icon https://www.google.com/s2/favicons?sz=64&domain=live.bilibili.com
  12. // @grant GM_getValue
  13. // @grant GM_setValue
  14. // @grant GM_registerMenuCommand
  15. // @grant GM_unregisterMenuCommand
  16. // @run-at document-body
  17. // ==/UserScript==
  18.  
  19. // @ts-check
  20.  
  21. /**
  22. * @typedef Logger
  23. * @property {(...args: any[]) => void} log log
  24. * @property {(...args: any[]) => void} warn warn
  25. * @property {(...args: any[]) => void} error error
  26. * @property {(...args: any[]) => void} info info
  27. */
  28.  
  29. /**
  30. * @typedef {() => boolean} Condition
  31. */
  32.  
  33. ;(function () {
  34. 'use strict'
  35.  
  36. const SCRIPT_NAME = 'clean_bilibili'
  37.  
  38. /**
  39. * Create logger
  40. * @returns {Logger} logger instance
  41. */
  42. function createLogger() {
  43. const prefix = `[${SCRIPT_NAME}]`
  44. return {
  45. log: (...args) => console.log(prefix, ...args),
  46. warn: (...args) => console.warn(prefix, ...args),
  47. error: (...args) => console.error(prefix, ...args),
  48. info: (...args) => console.info(prefix, ...args),
  49. }
  50. }
  51. const logger = createLogger()
  52.  
  53. /**
  54. * Wait for elements to be ready
  55. * @param {string} selector - selector to watch
  56. * @returns {Promise<Element[]>} elements
  57. */
  58. function waitForElements(selector) {
  59. return new Promise(resolve => {
  60. if (document.querySelectorAll(selector).length > 0) {
  61. resolve(Array.from(document.querySelectorAll(selector)))
  62. }
  63.  
  64. const observer = new MutationObserver(() => {
  65. if (document.querySelectorAll(selector).length > 0) {
  66. resolve(Array.from(document.querySelectorAll(selector)))
  67. observer.disconnect()
  68. }
  69. })
  70.  
  71. try {
  72. observer.observe(document.body, {
  73. childList: true,
  74. subtree: true,
  75. })
  76. } catch {
  77. // If failed, try again in 100ms
  78. setTimeout(() => {
  79. resolve(waitForElements(selector))
  80. }, 100)
  81. }
  82. })
  83. }
  84.  
  85. /**
  86. * Wait until conditions are met
  87. *
  88. * @param {Condition | Condition[]} conditions - conditions
  89. * @param {() => void} callback - callback
  90. */
  91. function waitUntil(conditions, callback) {
  92. const observer = new MutationObserver(() => {
  93. const matchCondition = Array.isArray(conditions)
  94. ? conditions.every(condition => condition())
  95. : conditions()
  96.  
  97. if (!matchCondition) return
  98.  
  99. callback()
  100. observer.disconnect()
  101. })
  102.  
  103. observer.observe(document.body, {
  104. childList: true,
  105. subtree: true,
  106. })
  107. }
  108.  
  109. /**
  110. * Create UI for the options
  111. * @param {string} key - option key
  112. * @param {string} title - option title
  113. * @param {boolean} defaultValue - option default-value
  114. * @returns {{ value: boolean }} value ref
  115. */
  116. function useOption(key, title, defaultValue) {
  117. if (typeof GM_getValue === 'undefined') {
  118. return {
  119. value: defaultValue,
  120. }
  121. }
  122. let value = GM_getValue(key, defaultValue)
  123. const ref = {
  124. get value() {
  125. return value
  126. },
  127. set value(v) {
  128. value = v
  129. GM_setValue(key, v)
  130. location.reload()
  131. },
  132. }
  133. GM_registerMenuCommand(`${title}: ${value ? '✅' : '❌'}`, () => {
  134. ref.value = !value
  135. })
  136. return ref
  137. }
  138.  
  139. const doc = document
  140.  
  141. // Up 主投稿 创作中心
  142. const HIDE_UP_ENTRY = useOption('bilibili_hide_up_entry', 'Hide UP Entry', true)
  143. // 话题 - 动态页面
  144. const HIDE_TOPIC_PANEL = useOption('bilibili_hide_topic_panel', 'Hide Topic Panel', true)
  145. // 显示关注时间
  146. const ENABLE_FOLLOW_TIME = useOption('bilibili_enable_follow_time', 'Enable Follow Time', true)
  147.  
  148. /** @type {string[]} */
  149. const HIDE_SELECTORS = [
  150. /**
  151. * ================================
  152. * 全局
  153. * ================================
  154. */
  155. // 游戏中心
  156. '.bili-header .left-entry .default-entry[href*="game.bilibili.com"]',
  157. // 会员购
  158. '.bili-header .left-entry .default-entry[href*="show.bilibili.com"]',
  159. // 漫画
  160. '.bili-header .left-entry .default-entry[href*="manga.bilibili.com"]',
  161. // 赛事
  162. '.bili-header .left-entry .default-entry[href*="www.bilibili.com/match"]',
  163. // 年度报告
  164. '.bili-header .left-entry .loc-entry.loc-moveclip',
  165. // 番剧
  166. '.bili-header .left-entry .default-entry[href*="www.bilibili.com/bangumi/play"]',
  167. '.bili-header .left-entry .left-loc-entry a[href*="www.bilibili.com/bangumi/play"]',
  168. // 下载客户端
  169. '.bili-header .left-entry .download-entry[href*="app.bilibili.com"]',
  170. // 桌面端提示
  171. '.desktop-download-tip',
  172.  
  173. /**
  174. * ================================
  175. * 个人动态
  176. * ================================
  177. */
  178. // 轮播广告
  179. '.bili-dyn-ads',
  180.  
  181. /**
  182. * ================================
  183. * 视频页面
  184. * ================================
  185. */
  186. // 右侧轮播
  187. '.slide-ad-exp', // also #slide_ad
  188. // 游戏推荐
  189. '.video-page-game-card-small',
  190. // 评论区顶部推荐 右侧推荐区底部
  191. '.ad-report.ad-floor-exp',
  192.  
  193. /**
  194. * ================================
  195. * 用户空间
  196. * ================================
  197. */
  198. // 最近玩儿过的游戏
  199. '.s-space .col-2 .section.game',
  200.  
  201. /**
  202. * ================================
  203. * 直播页面
  204. * ================================
  205. */
  206. '#head-info-vm .activity-gather-entry',
  207.  
  208. ...(HIDE_UP_ENTRY.value ? ['.bili-header .right-entry a[href*="member.bilibili.com"]'] : []),
  209.  
  210. ...(HIDE_TOPIC_PANEL.value ? ['.sticky .bili-dyn-topic-box'] : []),
  211. ].filter(Boolean)
  212.  
  213. /** @type {string[]} */
  214. const INJECTED_STYLE = [
  215. // 隐藏元素
  216. `${HIDE_SELECTORS.join(',')} { display: none !important;}`,
  217. ]
  218.  
  219. async function onUserSpace() {
  220. if (!ENABLE_FOLLOW_TIME.value) {
  221. return logger.info('Follow time disabled')
  222. }
  223.  
  224. if (/^https?:\/\/space\.bilibili\.com\/\d{3,}/.test(location.href)) {
  225. const getSpaceMid = () => location.pathname.split('/').at(1)
  226. const mid = getSpaceMid()
  227.  
  228. if (!mid) return
  229.  
  230. /** @type any */
  231. let res = await unsafeWindow.fetch(
  232. `https://api.bilibili.com/x/space/acc/relation?mid=${mid}`,
  233. {
  234. credentials: 'include',
  235. },
  236. )
  237. res = await res.json()
  238.  
  239. const mtime = /** @type {number?} */ (res?.data?.relation?.mtime)
  240.  
  241. if (!mtime) return logger.error('Failed to get follow time')
  242.  
  243. const result = new Date(mtime * 1000).toLocaleString()
  244.  
  245. const panel = doc.querySelector('.s-space .col-2')
  246.  
  247. if (!panel) return
  248. const section = doc.createElement('div')
  249.  
  250. section.classList.add('section')
  251. section.innerHTML = `
  252. <span>关注时间:</span>
  253. <strong>${result}</strong>
  254. `
  255.  
  256. panel.prepend(section)
  257. }
  258. }
  259.  
  260. async function hideShadowDOM() {
  261. const styleSheet = new CSSStyleSheet()
  262. styleSheet.replaceSync('#notice { display: none !important; }')
  263. document
  264. .querySelector('#commentapp bili-comments')
  265. ?.shadowRoot?.querySelector('bili-comments-header-renderer')
  266. ?.shadowRoot?.adoptedStyleSheets.push(styleSheet)
  267. }
  268.  
  269. async function registerSevices() {
  270. waitForElements('#page-dynamic').then(onUserSpace)
  271. waitUntil(
  272. () => !!document.querySelector('#commentapp bili-comments'),
  273. () => {
  274. hideShadowDOM()
  275. },
  276. )
  277. }
  278.  
  279. /**
  280. * Inject style
  281. */
  282. function injectStyle() {
  283. const el = doc.createElement('style')
  284.  
  285. el.id = `${SCRIPT_NAME}_style`
  286. el.innerHTML = INJECTED_STYLE.join('')
  287. doc.head.append(el)
  288.  
  289. logger.info('Style injected')
  290. }
  291.  
  292. if (doc.readyState === 'loading') {
  293. doc.addEventListener('DOMContentLoaded', () => {
  294. injectStyle()
  295. registerSevices()
  296. })
  297. } else {
  298. injectStyle()
  299. registerSevices()
  300. }
  301. })()