🏠 返回首頁 

Greasy Fork is available in English.

bilibili三连

推荐投币收藏一键三连


Installer dette script?
  1. // ==UserScript==
  2. // @name bilibili三连
  3. // @version 0.0.22
  4. // @include https://www.bilibili.com/video/av*
  5. // @include https://www.bilibili.com/video/BV*
  6. // @include https://www.bilibili.com/medialist/play/*
  7. // @description 推荐投币收藏一键三连
  8. // @grant GM_getValue
  9. // @grant GM_setValue
  10. // @grant GM_addValueChangeListener
  11. // @run-at document-idle
  12. // @namespace https://greasyfork.org/users/164996
  13. // ==/UserScript==
  14. const find = (selector) => {
  15. return document.querySelector(selector)
  16. }
  17. const click = (s) => {
  18. if (!s) return
  19. if (s instanceof HTMLElement) s.click()
  20. else {
  21. const n = document.querySelector(s)
  22. if (!n) return
  23. n.click()
  24. }
  25. return true
  26. }
  27. const waitForAllByObserver = (
  28. selectors,
  29. {
  30. app = document.documentElement,
  31. timeout = 3000,
  32. childList = true,
  33. subtree = true,
  34. attributes = true,
  35. disappear = false,
  36. } = {}
  37. ) => {
  38. return new Promise((resolve) => {
  39. let observer_id
  40. let timer_id
  41. const check = () => {
  42. const nodes = selectors.map((i) => document.querySelector(i))
  43. if (Object.values(nodes).every((v) => (disappear ? !v : v))) {
  44. if (observer_id != undefined) observer_id.disconnect()
  45. if (timer_id != undefined) clearTimeout(timer_id)
  46. resolve(nodes)
  47. }
  48. }
  49. if (check()) return
  50. observer_id = new MutationObserver(check)
  51. if (timeout != Infinity) {
  52. timer_id = setTimeout(() => {
  53. observer_id.disconnect()
  54. clearTimeout(timer_id)
  55. resolve()
  56. }, timeout)
  57. }
  58. observer_id.observe(app, { childList, subtree, attributes })
  59. })
  60. }
  61. const sleep = (timeout) =>
  62. new Promise((resolve) => {
  63. setTimeout(resolve, timeout)
  64. })
  65. const state = {
  66. get(k) {
  67. return this.state[k]
  68. },
  69. set(k, v) {
  70. this.state[k] = v
  71. this.render()
  72. GM_setValue('state', JSON.stringify(this.state))
  73. },
  74. toggle(k) {
  75. this.set(k, !this.state[k])
  76. },
  77. state: {},
  78. node: {},
  79. default_state: {
  80. like: true,
  81. coin: 0,
  82. collect: true,
  83. collection: '输入收藏夹名',
  84. },
  85. render() {
  86. const { like, coin, coin_value, collect, collection } = this.node
  87. const get = this.get.bind(this)
  88. if (get('like')) like.classList.add('sanlian_on')
  89. else like.classList.remove('sanlian_on')
  90. if (get('coin')) coin.classList.add('sanlian_on')
  91. else coin.classList.remove('sanlian_on')
  92. coin_value.innerHTML = 'x' + get('coin')
  93. if (get('collect')) collect.classList.add('sanlian_on')
  94. else collect.classList.remove('sanlian_on')
  95. collection.value = get('collection')
  96. },
  97. load(state_str) {
  98. try {
  99. this.state = JSON.parse(state_str)
  100. for (let k of Object.keys(this.default_state)) {
  101. if (typeof this.default_state[k] != typeof this.state[k]) {
  102. throw `${k}'s type is not same as default`
  103. }
  104. }
  105. } catch (e) {
  106. this.state = { ...this.default_state }
  107. }
  108. this.render()
  109. },
  110. remove_coin_leading_space() {
  111. const trim = () => {
  112. const coin_text = document.querySelector(this.selector.coin + ' i')
  113. .nextSibling
  114. if (
  115. coin_text.nodeType == Node.TEXT_NODE &&
  116. coin_text.textContent != coin_text.textContent.trim()
  117. ) {
  118. coin_text.textContent = coin_text.textContent.trim()
  119. }
  120. }
  121. new MutationObserver(trim).observe(
  122. document.querySelector(this.selector.coin),
  123. { characterData: true, subtree: true }
  124. )
  125. trim()
  126. },
  127. addStyle() {
  128. const css = `
  129. #sanlian > div {
  130. display: none;
  131. position: absolute;
  132. color: SlateGray;
  133. background: white;
  134. border: 1px solid #e5e9ef;
  135. box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.14);
  136. border-radius: 2px;
  137. padding: 1em;
  138. cursor: default;
  139. z-index: 2;
  140. }
  141. #sanlian_like {
  142. margin: 0 1em 0 0;
  143. }
  144. #sanlian_coin {
  145. margin: 0 1em 0 0;
  146. }
  147. #sanlian input {
  148. color: SlateGrey;
  149. cursor: text;
  150. }
  151. #sanlian span[id^='sanlian_'] * {
  152. color: SlateGrey;
  153. cursor: pointer;
  154. user-select: none;
  155. }
  156. #sanlian span[id^='sanlian_'].sanlian_on * {
  157. color: SlateBlue;
  158. }
  159. #sanlian span[id^='sanlian_']:hover * {
  160. color: DarkSlateBlue;
  161. }
  162. #sanlian > div > input {
  163. border: 0;
  164. border-bottom: 1px solid;
  165. }
  166. #sanlian span#sanlian_coin i {
  167. margin: 0;
  168. }
  169. #sanlian > i.iconfont {
  170. margin-left: -1em;
  171. transform-origin: right;
  172. transform: scale(0.4, 0.8);
  173. display: inline-block;
  174. }
  175. .video-toolbar .ops > span {
  176. width: 88px;
  177. }
  178. ${this.selector.coin_dialog}, ${this.selector.collect_dialog} {
  179. display: block;
  180. }
  181. `
  182. const style = document.createElement('style')
  183. style.type = 'text/css'
  184. style.appendChild(document.createTextNode(css))
  185. document.head.appendChild(style)
  186. const rules = style.sheet.rules
  187. this.node.dialog_style = rules[rules.length - 1].style
  188. this.remove_coin_leading_space()
  189. },
  190. addNode() {
  191. const { collect } = this.node
  192. const { selector } = this
  193. const sanlian = collect.cloneNode(true)
  194. const sanlian_icon = sanlian.querySelector('i')
  195. const sanlian_text =
  196. sanlian_icon.nextElementSibling || sanlian_icon.nextSibling
  197. sanlian.id = 'sanlian'
  198. sanlian.classList.remove('on')
  199. sanlian.title = '推荐硬币收藏'
  200. const sanlian_canvas = sanlian.querySelector('canvas')
  201. if (sanlian_canvas) sanlian_canvas.remove()
  202. sanlian_icon.innerText = ''
  203. sanlian_icon.classList.remove('blue')
  204. sanlian_icon.classList.add('van-icon-tuodong')
  205. sanlian_text.textContent = '三连'
  206. const sanlian_panel = document.createElement('div')
  207. for (const name of ['like', 'coin', 'collect']) {
  208. const wrapper = document.createElement('span')
  209. wrapper.id = `sanlian_${name}`
  210. const node = document.querySelector(selector[name] + ' i').cloneNode(true)
  211. node.classList.remove('blue')
  212. wrapper.appendChild(node)
  213. if (name == 'coin') {
  214. wrapper.insertAdjacentHTML('beforeend', `<span>x${state.coin}</span>`)
  215. }
  216. sanlian_panel.appendChild(wrapper)
  217. this.node[name] = wrapper
  218. }
  219. sanlian_panel.insertAdjacentHTML('beforeend', `<input type="text">`)
  220. sanlian.appendChild(sanlian_panel)
  221. collect.parentNode.insertBefore(sanlian, collect.nextSibling)
  222. Object.assign(this.node, {
  223. coin_value: document.querySelector('#sanlian_coin span'),
  224. collection: document.querySelector('#sanlian input'),
  225. sanlian,
  226. sanlian_icon,
  227. sanlian_text,
  228. sanlian_panel,
  229. })
  230. },
  231. addListener() {
  232. const {
  233. app,
  234. coin,
  235. collect,
  236. collection,
  237. dialog_style,
  238. like,
  239. sanlian,
  240. sanlian_icon,
  241. sanlian_panel,
  242. sanlian_text,
  243. } = this.node
  244. const {
  245. coin_close,
  246. coin_dialog,
  247. coin_left,
  248. coin_off,
  249. coin_right,
  250. coin_yes,
  251. collect_choice,
  252. collect_close,
  253. collect_dialog,
  254. collect_yes,
  255. like_off,
  256. } = this.selector
  257. const selector = this.selector
  258. const get = this.get.bind(this)
  259. const set = this.set.bind(this)
  260. const toggle = this.toggle.bind(this)
  261. like.addEventListener('click', function () {
  262. toggle('like')
  263. })
  264. coin.addEventListener('click', function () {
  265. set('coin', (get('coin') + 1) % 3)
  266. })
  267. collect.addEventListener('click', function () {
  268. toggle('collect')
  269. })
  270. like.addEventListener('contextmenu', function () {
  271. toggle('like')
  272. })
  273. coin.addEventListener('contextmenu', function () {
  274. set('coin', (get('coin') + 2) % 3)
  275. })
  276. collect.addEventListener('contextmenu', function () {
  277. toggle('collect')
  278. })
  279. collection.addEventListener('keyup', function () {
  280. set('collection', collection.value)
  281. })
  282. sanlian.addEventListener('mouseover', () => {
  283. sanlian_panel.style.display = 'flex'
  284. })
  285. sanlian.addEventListener('mouseout', () => {
  286. sanlian_panel.style.display = 'none'
  287. })
  288. const like_handler = async () => {
  289. if (get('like')) click(like_off)
  290. }
  291. const coin_handler = async () => {
  292. if (!get('coin') > 0 || !click(coin_off)) return
  293. if (!(await waitForAllByObserver([coin_left]))) return
  294. if (get('coin') === 1) click(coin_left)
  295. else click(coin_right)
  296. await sleep(0) // only for visual updating
  297. click(coin_yes)
  298. await Promise.race([
  299. waitForAllByObserver([coin_dialog], { disappear: true }),
  300. waitForAllByObserver(['.error']),
  301. ])
  302. click(coin_close)
  303. }
  304. const collect_handler = async () => {
  305. if (
  306. !get('collect') ||
  307. !click(selector.collect) ||
  308. !(await waitForAllByObserver([collect_choice]))
  309. ) {
  310. click('i.close')
  311. return
  312. }
  313. const choices = document.querySelectorAll(selector.collect_choice)
  314. const choice =
  315. [...choices].find(
  316. (i) => i.nextElementSibling.textContent.trim() === get('collection')
  317. ) || choices[0]
  318. // already collect
  319. if (
  320. !choice ||
  321. choice.previousElementSibling.checked ||
  322. !click(choice) ||
  323. !(await waitForAllByObserver([collect_yes]))
  324. ) {
  325. click('i.close')
  326. return
  327. }
  328. click(collect_yes)
  329. await waitForAllByObserver([collect_dialog], { disappear: true })
  330. }
  331. sanlian.addEventListener('click', async (e) => {
  332. if (![sanlian, sanlian_icon, sanlian_text].includes(e.target)) return
  333. dialog_style.display = 'none'
  334. const fallback = setTimeout(() => {
  335. dialog_style.display = 'block'
  336. }, 3500)
  337. await like_handler()
  338. await coin_handler()
  339. await collect_handler()
  340. clearTimeout(fallback)
  341. dialog_style.display = 'block'
  342. })
  343. },
  344. selector: {
  345. app: 'div#app',
  346. coin: '#arc_toolbar_report span.coin',
  347. coin_close: 'div.bili-dialog-m div.coin-operated-m i.close',
  348. collect_close: 'div.bili-dialog-m div.collection-m i.close',
  349. coin_dialog: '.bili-dialog-m',
  350. coin_left: '.mc-box.left-con',
  351. coin_off: '#arc_toolbar_report span.coin:not(.on)',
  352. coin_right: '.mc-box.right-con',
  353. coin_yes: 'div.coin-bottom > span',
  354. collect: '#arc_toolbar_report span.collect',
  355. collect_choice: 'div.collection-m div.group-list input+i',
  356. collect_dialog: '.bili-dialog-m',
  357. collect_off: '#arc_toolbar_report span.collect:not(.on)',
  358. collect_yes: 'div.collection-m button.submit-move:not([disable])',
  359. like: '#arc_toolbar_report span.like',
  360. like_off: '#arc_toolbar_report span.like:not(.on)',
  361. people: 'div.bilibili-player-video-info-people-number',
  362. },
  363. async init() {
  364. let { collect, app, people } = this.selector
  365. ;[collect, app, people] = await waitForAllByObserver(
  366. [collect, app, people],
  367. { timeout: Infinity }
  368. )
  369. if (!collect) return
  370. Object.assign(this.node, { collect, app })
  371. this.addStyle()
  372. this.addNode()
  373. this.addListener()
  374. this.load(GM_getValue('state'))
  375. GM_addValueChangeListener('state', (name, old_state, new_state) => {
  376. if (JSON.stringify(this.state) == new_state) return
  377. this.load(new_state)
  378. })
  379. },
  380. }
  381. state.init()