Greasy Fork is available in English.
推荐投币收藏一键三连
- // ==UserScript==
- // @name bilibili三连
- // @version 0.0.22
- // @include https://www.bilibili.com/video/av*
- // @include https://www.bilibili.com/video/BV*
- // @include https://www.bilibili.com/medialist/play/*
- // @description 推荐投币收藏一键三连
- // @grant GM_getValue
- // @grant GM_setValue
- // @grant GM_addValueChangeListener
- // @run-at document-idle
- // @namespace https://greasyfork.org/users/164996
- // ==/UserScript==
- const find = (selector) => {
- return document.querySelector(selector)
- }
- const click = (s) => {
- if (!s) return
- if (s instanceof HTMLElement) s.click()
- else {
- const n = document.querySelector(s)
- if (!n) return
- n.click()
- }
- return true
- }
- const waitForAllByObserver = (
- selectors,
- {
- app = document.documentElement,
- timeout = 3000,
- childList = true,
- subtree = true,
- attributes = true,
- disappear = false,
- } = {}
- ) => {
- return new Promise((resolve) => {
- let observer_id
- let timer_id
- const check = () => {
- const nodes = selectors.map((i) => document.querySelector(i))
- if (Object.values(nodes).every((v) => (disappear ? !v : v))) {
- if (observer_id != undefined) observer_id.disconnect()
- if (timer_id != undefined) clearTimeout(timer_id)
- resolve(nodes)
- }
- }
- if (check()) return
- observer_id = new MutationObserver(check)
- if (timeout != Infinity) {
- timer_id = setTimeout(() => {
- observer_id.disconnect()
- clearTimeout(timer_id)
- resolve()
- }, timeout)
- }
- observer_id.observe(app, { childList, subtree, attributes })
- })
- }
- const sleep = (timeout) =>
- new Promise((resolve) => {
- setTimeout(resolve, timeout)
- })
- const state = {
- get(k) {
- return this.state[k]
- },
- set(k, v) {
- this.state[k] = v
- this.render()
- GM_setValue('state', JSON.stringify(this.state))
- },
- toggle(k) {
- this.set(k, !this.state[k])
- },
- state: {},
- node: {},
- default_state: {
- like: true,
- coin: 0,
- collect: true,
- collection: '输入收藏夹名',
- },
- render() {
- const { like, coin, coin_value, collect, collection } = this.node
- const get = this.get.bind(this)
- if (get('like')) like.classList.add('sanlian_on')
- else like.classList.remove('sanlian_on')
- if (get('coin')) coin.classList.add('sanlian_on')
- else coin.classList.remove('sanlian_on')
- coin_value.innerHTML = 'x' + get('coin')
- if (get('collect')) collect.classList.add('sanlian_on')
- else collect.classList.remove('sanlian_on')
- collection.value = get('collection')
- },
- load(state_str) {
- try {
- this.state = JSON.parse(state_str)
- for (let k of Object.keys(this.default_state)) {
- if (typeof this.default_state[k] != typeof this.state[k]) {
- throw `${k}'s type is not same as default`
- }
- }
- } catch (e) {
- this.state = { ...this.default_state }
- }
- this.render()
- },
- remove_coin_leading_space() {
- const trim = () => {
- const coin_text = document.querySelector(this.selector.coin + ' i')
- .nextSibling
- if (
- coin_text.nodeType == Node.TEXT_NODE &&
- coin_text.textContent != coin_text.textContent.trim()
- ) {
- coin_text.textContent = coin_text.textContent.trim()
- }
- }
- new MutationObserver(trim).observe(
- document.querySelector(this.selector.coin),
- { characterData: true, subtree: true }
- )
- trim()
- },
- addStyle() {
- const css = `
- #sanlian > div {
- display: none;
- position: absolute;
- color: SlateGray;
- background: white;
- border: 1px solid #e5e9ef;
- box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.14);
- border-radius: 2px;
- padding: 1em;
- cursor: default;
- z-index: 2;
- }
- #sanlian_like {
- margin: 0 1em 0 0;
- }
- #sanlian_coin {
- margin: 0 1em 0 0;
- }
- #sanlian input {
- color: SlateGrey;
- cursor: text;
- }
- #sanlian span[id^='sanlian_'] * {
- color: SlateGrey;
- cursor: pointer;
- user-select: none;
- }
- #sanlian span[id^='sanlian_'].sanlian_on * {
- color: SlateBlue;
- }
- #sanlian span[id^='sanlian_']:hover * {
- color: DarkSlateBlue;
- }
- #sanlian > div > input {
- border: 0;
- border-bottom: 1px solid;
- }
- #sanlian span#sanlian_coin i {
- margin: 0;
- }
- #sanlian > i.iconfont {
- margin-left: -1em;
- transform-origin: right;
- transform: scale(0.4, 0.8);
- display: inline-block;
- }
- .video-toolbar .ops > span {
- width: 88px;
- }
- ${this.selector.coin_dialog}, ${this.selector.collect_dialog} {
- display: block;
- }
- `
- const style = document.createElement('style')
- style.type = 'text/css'
- style.appendChild(document.createTextNode(css))
- document.head.appendChild(style)
- const rules = style.sheet.rules
- this.node.dialog_style = rules[rules.length - 1].style
- this.remove_coin_leading_space()
- },
- addNode() {
- const { collect } = this.node
- const { selector } = this
- const sanlian = collect.cloneNode(true)
- const sanlian_icon = sanlian.querySelector('i')
- const sanlian_text =
- sanlian_icon.nextElementSibling || sanlian_icon.nextSibling
- sanlian.id = 'sanlian'
- sanlian.classList.remove('on')
- sanlian.title = '推荐硬币收藏'
- const sanlian_canvas = sanlian.querySelector('canvas')
- if (sanlian_canvas) sanlian_canvas.remove()
- sanlian_icon.innerText = ''
- sanlian_icon.classList.remove('blue')
- sanlian_icon.classList.add('van-icon-tuodong')
- sanlian_text.textContent = '三连'
- const sanlian_panel = document.createElement('div')
- for (const name of ['like', 'coin', 'collect']) {
- const wrapper = document.createElement('span')
- wrapper.id = `sanlian_${name}`
- const node = document.querySelector(selector[name] + ' i').cloneNode(true)
- node.classList.remove('blue')
- wrapper.appendChild(node)
- if (name == 'coin') {
- wrapper.insertAdjacentHTML('beforeend', `<span>x${state.coin}</span>`)
- }
- sanlian_panel.appendChild(wrapper)
- this.node[name] = wrapper
- }
- sanlian_panel.insertAdjacentHTML('beforeend', `<input type="text">`)
- sanlian.appendChild(sanlian_panel)
- collect.parentNode.insertBefore(sanlian, collect.nextSibling)
- Object.assign(this.node, {
- coin_value: document.querySelector('#sanlian_coin span'),
- collection: document.querySelector('#sanlian input'),
- sanlian,
- sanlian_icon,
- sanlian_text,
- sanlian_panel,
- })
- },
- addListener() {
- const {
- app,
- coin,
- collect,
- collection,
- dialog_style,
- like,
- sanlian,
- sanlian_icon,
- sanlian_panel,
- sanlian_text,
- } = this.node
- const {
- coin_close,
- coin_dialog,
- coin_left,
- coin_off,
- coin_right,
- coin_yes,
- collect_choice,
- collect_close,
- collect_dialog,
- collect_yes,
- like_off,
- } = this.selector
- const selector = this.selector
- const get = this.get.bind(this)
- const set = this.set.bind(this)
- const toggle = this.toggle.bind(this)
- like.addEventListener('click', function () {
- toggle('like')
- })
- coin.addEventListener('click', function () {
- set('coin', (get('coin') + 1) % 3)
- })
- collect.addEventListener('click', function () {
- toggle('collect')
- })
- like.addEventListener('contextmenu', function () {
- toggle('like')
- })
- coin.addEventListener('contextmenu', function () {
- set('coin', (get('coin') + 2) % 3)
- })
- collect.addEventListener('contextmenu', function () {
- toggle('collect')
- })
- collection.addEventListener('keyup', function () {
- set('collection', collection.value)
- })
- sanlian.addEventListener('mouseover', () => {
- sanlian_panel.style.display = 'flex'
- })
- sanlian.addEventListener('mouseout', () => {
- sanlian_panel.style.display = 'none'
- })
- const like_handler = async () => {
- if (get('like')) click(like_off)
- }
- const coin_handler = async () => {
- if (!get('coin') > 0 || !click(coin_off)) return
- if (!(await waitForAllByObserver([coin_left]))) return
- if (get('coin') === 1) click(coin_left)
- else click(coin_right)
- await sleep(0) // only for visual updating
- click(coin_yes)
- await Promise.race([
- waitForAllByObserver([coin_dialog], { disappear: true }),
- waitForAllByObserver(['.error']),
- ])
- click(coin_close)
- }
- const collect_handler = async () => {
- if (
- !get('collect') ||
- !click(selector.collect) ||
- !(await waitForAllByObserver([collect_choice]))
- ) {
- click('i.close')
- return
- }
- const choices = document.querySelectorAll(selector.collect_choice)
- const choice =
- [...choices].find(
- (i) => i.nextElementSibling.textContent.trim() === get('collection')
- ) || choices[0]
- // already collect
- if (
- !choice ||
- choice.previousElementSibling.checked ||
- !click(choice) ||
- !(await waitForAllByObserver([collect_yes]))
- ) {
- click('i.close')
- return
- }
- click(collect_yes)
- await waitForAllByObserver([collect_dialog], { disappear: true })
- }
- sanlian.addEventListener('click', async (e) => {
- if (![sanlian, sanlian_icon, sanlian_text].includes(e.target)) return
- dialog_style.display = 'none'
- const fallback = setTimeout(() => {
- dialog_style.display = 'block'
- }, 3500)
- await like_handler()
- await coin_handler()
- await collect_handler()
- clearTimeout(fallback)
- dialog_style.display = 'block'
- })
- },
- selector: {
- app: 'div#app',
- coin: '#arc_toolbar_report span.coin',
- coin_close: 'div.bili-dialog-m div.coin-operated-m i.close',
- collect_close: 'div.bili-dialog-m div.collection-m i.close',
- coin_dialog: '.bili-dialog-m',
- coin_left: '.mc-box.left-con',
- coin_off: '#arc_toolbar_report span.coin:not(.on)',
- coin_right: '.mc-box.right-con',
- coin_yes: 'div.coin-bottom > span',
- collect: '#arc_toolbar_report span.collect',
- collect_choice: 'div.collection-m div.group-list input+i',
- collect_dialog: '.bili-dialog-m',
- collect_off: '#arc_toolbar_report span.collect:not(.on)',
- collect_yes: 'div.collection-m button.submit-move:not([disable])',
- like: '#arc_toolbar_report span.like',
- like_off: '#arc_toolbar_report span.like:not(.on)',
- people: 'div.bilibili-player-video-info-people-number',
- },
- async init() {
- let { collect, app, people } = this.selector
- ;[collect, app, people] = await waitForAllByObserver(
- [collect, app, people],
- { timeout: Infinity }
- )
- if (!collect) return
- Object.assign(this.node, { collect, app })
- this.addStyle()
- this.addNode()
- this.addListener()
- this.load(GM_getValue('state'))
- GM_addValueChangeListener('state', (name, old_state, new_state) => {
- if (JSON.stringify(this.state) == new_state) return
- this.load(new_state)
- })
- },
- }
- state.init()