移动端扫地机

F**k!クッソ

// ==UserScript==
// @name         移动端扫地机
// @namespace    https://greasyfork.org/
// @version      0.0.2-BETA
// @description  F**k!クッソ
// @author       fpschen
// @homepage     https://greasyfork.org/zh-CN/users/256892-fork
// @match        *://*.zhihu.com/*
// @match        *://m.bilibili.com/*
// @icon         https://www.bilibili.com/favicon.ico
// @grant        GM_addStyle
// @grant        unsafeWindow
// @run-at       document_idle
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    function autoCatch(caller, catcher, quiet=false) {
        return function() {
            try {
                return caller.apply(this, arguments)
            } catch(e) {
                catcher && catcher(e)
                if (!quiet) throw e
            }
        }
    }

    function reduceCall(...funcs) {
        return function() {
            funcs.reduce((p, c) => autoCatch(c, null, true)(), null)
        }
    }

    function listenHistory(name, listener) {
        const origin = history[name]
        history[name] = function() {
            const res = origin.apply(this, arguments)
            autoCatch(listener, null, true)(arguments)
            return res
        }
    }

    function findEle(selector, multi = false, container = document) {
        let finder = container.querySelector
        if (multi) {
            finder = container.querySelectorAll
        }
        return finder.call(container, selector)
    }

    async function waitEle(selector, { multi, timeout, quiet, container } = {}) {
        multi = multi ?? false
        timeout = timeout ?? 30000
        container = container ?? document
        const start = Date.now()
        return new Promise((resolve, reject) => {
            let checker = () => {
                const el = findEle(selector, multi, container)
                if (el == undefined || (multi && el.length == 0)) {
                    if (Date.now() - start > timeout) {
                        throw new Error(`[selector](${selector}): timeout!`)
                    }
                    setTimeout(checker, 100)
                    return
                }
                resolve(el)
            }
            checker = autoCatch(checker, reject, quiet)
            checker()
        })
    }

    function createUnmuteButton() {
        if (document.getElementById('unmuteButton')) return

        const video = document.querySelector('video')
        if (!video.muted) {
            return
        }

        GM_addStyle(`
/*
* 声音按钮 *
*/

.unmute {
  position: absolute;
  top: 0;
  padding: 12px;
  background: none;
  border: 0;
  font-size: 127%;
  text-align: inherit;
}
.unmute-inner {
  position: relative;
}
.unmute-icon {
  height: 48px;
  display: inline-block;
  vertical-align: middle;
  padding-left: 2px;
  position: relative;
  z-index: 10;
  background-color: rgb(255, 255, 255);
  border-radius: 2px;
  border-bottom: 1px solid #f1f1f1;
}
.unmute svg {
  filter: drop-shadow(0 0 2px rgba(0,0,0,.5));
}
.unmute-text {
  position: relative;
  z-index: 10;
  padding-right: 10px;
  vertical-align: middle;
  display: inline-block;
  transition: opacity .25s cubic-bezier(.4,0,1,1);
}
.animated .unmute-text {
  opacity: 0;
}
.unmute-box {
  width: 100%;
  background-color: rgb(255, 255, 255);
  position: absolute;
  top: 0;
  bottom: 0;
  border-radius: 2px;
  border-bottom: 1px solid #f1f1f1;
  transition: width .5s cubic-bezier(.4,0,1,1);
}
.animated .unmute-box {
  width: 0;
}
        `)

        const button = document.createElement('button')
        button.classList.add('unmute')
        button.id = 'unmuteButton'
        button.innerHTML = `
<div class="unmute-inner">
    <div class="unmute-icon"><svg height="100%" version="1.1" viewBox="0 0 36 36" width="100%">
            <use class="svg-shadow" xlink:href="#ytp-id-1"></use>
            <path class="ytp-svg-fill"
                d="m 21.48,17.98 c 0,-1.77 -1.02,-3.29 -2.5,-4.03 v 2.21 l 2.45,2.45 c .03,-0.2 .05,-0.41 .05,-0.63 z m 2.5,0 c 0,.94 -0.2,1.82 -0.54,2.64 l 1.51,1.51 c .66,-1.24 1.03,-2.65 1.03,-4.15 0,-4.28 -2.99,-7.86 -7,-8.76 v 2.05 c 2.89,.86 5,3.54 5,6.71 z M 9.25,8.98 l -1.27,1.26 4.72,4.73 H 7.98 v 6 H 11.98 l 5,5 v -6.73 l 4.25,4.25 c -0.67,.52 -1.42,.93 -2.25,1.18 v 2.06 c 1.38,-0.31 2.63,-0.95 3.69,-1.81 l 2.04,2.05 1.27,-1.27 -9,-9 -7.72,-7.72 z m 7.72,.99 -2.09,2.08 2.09,2.09 V 9.98 z"
                id="id-1"></path>
        </svg></div>
    <div class="unmute-text">点按取消静音</div>
    <div class="unmute-box"></div>
</div>
          `
        button.addEventListener('click', function () {
            video.muted = false
            button.remove()
        })

        const videoWrapper = document.querySelector('.mplayer-video-wrap')
        videoWrapper.insertAdjacentElement('afterend', button)
        setTimeout(() => {
            button.classList.add('animated')
        }, 4500)
    }

    // 清除APP引导弹窗
    async function clearModal() {
        GM_addStyle(`
        .OpenInAppButton, .home-float-openapp, .m-video2-awaken-btn {
          display: none!important;
        }

        `)

        const selectors = ['.MobileModal-wrapper button.Button--secondary', '.Modal-wrapper button.Modal-closeButton']

        const el = await Promise.any(selectors.map(selector => waitEle(selector, { quiet: true })))

        el?.click()
    }

    // 自动【展开阅读】
    function autoExpand() {
        GM_addStyle(`
        .Post-RichTextContainer div:has(.ContentItem-expandButton):not(:has(div)) {
          display: none!important;
        }
        button.ContentItem-expandButton {
          display: none!important;
        }
        .RichContent-inner--collapsed {
          max-height: unset!important;
          mask-image: unset!important;
          --webkit-mask-image: unset!important;
        }
        `)
    }

    // 清除B站推荐视频打开APP
    async function directJump() {
        const cards = await waitEle('.v-card-toapp', { multi: true, quiet: true })

        function assignUrl(card) {
            const vm = card?.__vue__
            const info = vm?.info
            vm?.$set(vm?.info, 'url', `https://${location.host}/video/${info?.bvid}`)
        }

        cards?.forEach(assignUrl)
    }

    // 自动播放
    async function autoPlay() {

        const selectors = ['.natural-main-video', '.m-video-player']

        const video = await Promise.any(selectors.map(selector => waitEle(selector, { quiet: true })))
        const vm = video?.__vue__
        vm?.$set(vm, 'open', true)
        vm?.$set(vm, 'bsource', 'search_google')
        vm?.$emit('trigglePlay')

        player.on('video_media_play', createUnmuteButton)

        await bindPlaybackspeed()

    }

    // 自动关闭弹框
    async function autoCloseDialog() {
        const closeBtn = await waitEle('.openapp-dialog .dialog-close', { quiet: true })
        closeBtn?.click()
    }

    async function bindPlaybackspeed() {
        const btn = await waitEle('.mplayer-control-btn-speed', { quiet: true, timeout: 500 })
        if (btn.__bound) {
            return
        }

        btn.__bound = true

        const speedList = [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 2.5, 3]

        const speedContainer = document.createElement('div')
        const containerList = speedContainer.classList
        containerList.add('speed-control-container', 'display-none')
        btn.appendChild(speedContainer)

        btn.addEventListener('click', () => {
            if (containerList.contains('display-none')) {
                containerList.remove('display-none')
            } else {
                containerList.add('display-none')
            }
        })

        function speedClick({ target }) {
            const video = document.querySelector('video')
            const speed = Number(target.getAttribute('data-speed'))
            if (video && speed > 0) {
                video.playbackRate = speed
            }
        }

        function addSpeed(speed, container) {
            const speedSpan = document.createElement('span')
            speedSpan.innerText = `${speed} X`
            speedSpan.setAttribute('data-speed', speed)
            speedSpan.addEventListener('click', speedClick)
            container.insertAdjacentElement('afterbegin', speedSpan)
        }

        speedList.map(speed => addSpeed(speed, speedContainer))

        GM_addStyle(`
  .speed-control-container {
    display: flex;
    position: absolute;
    font-size: var(--show-size);
    color: white;
    flex-direction: column;
    transform: translateY(-75%);
    max-height: var(--show-height);
    overflow-y: scroll;
    background-color: rgba(0, 0, 0, 0.5);
    padding: .2rem;
    padding-right: .3rem;
    border-radius: .4rem;
    white-space: pre;
    text-align: right;

    --show-count: 8;
    --total-count: ${speedList.length};
    --show-size: .8rem;
    --show-height: calc(var(--show-count) * var(--show-size));
  }
  .display-none {
    display: none;
  }
  `)

    }

    async function observeCardBox(listen = true) {


        if (listen) {
            const changeListener = reduceCall(observeCardBox.bind(null, !listen), autoPlay)

            listenHistory('pushState', changeListener)
            listenHistory('replaceState', changeListener)
        }

        const videoList = await waitEle('.video-list', { quiet: true })
        const vm = videoList?.__vue__
        vm?.$watch('list', () => {
            directJump()
        })
        const state = vm?.$store?.state

        const common = state?.common
        if (common) {
            // common.noCallApp = true
        }

        const search = state?.search
        if (search) {
            search.openAppDialog = false
        }
        const video = state?.video
        if (video) {
            video.isClient = true
        }

        if (PlayerAgent) {
            PlayerAgent.openApp = function() {
                console.log('call open app')
            }
        }
    }

    autoCatch(clearModal, null, true)()
    autoExpand()
    directJump()
    autoPlay()
    autoCloseDialog()
    observeCardBox()
})();