Scroll wheel volume control, "Are you there" popup bypass, Infinite autoplay, Volume saving
目前為
// ==UserScript==
// @name Youtube Better Player
// @description Scroll wheel volume control, "Are you there" popup bypass, Infinite autoplay, Volume saving
// @match https://www.youtube.com/*
// @grant GM_getValue
// @grant GM_setValue
// @version 0.0.1.20210129083149
// @namespace https://greasyfork.org/users/286737
// ==/UserScript==
class Player {
constructor() {
this.isEmbed = location.pathname.startsWith('/embed/')
this.volumeSk = 'volume'
}
async init() {
const isEmbed = this.isEmbed
const api = this.api = await (isEmbed ? this.getApiEmbed() : this.getApi())
api.setVolume(GM_getValue(this.volumeSk))
this.volume = api.getVolume()
const {$video, $eventCatcher, $volumeBar, $player} = this.getEls()
this.listenEvents($video)
new WheelVolume(this, api, $volumeBar, $player).init($eventCatcher)
if (!isEmbed) new RealAutoPlay(api).init()
}
async getApi() {
let $el, api
while (!($el = unsafeWindow['ytd-player'])) await wait(1000)
while (!(api = $el.player_)) await wait(200)
while (!api.isReady()) await wait(200)
return api
}
async getApiEmbed() {
let api
while (!(api = unsafeWindow.movie_player)) await wait(1000)
// api addEventListener don't support {once}
await new Promise(r => {
const onStateChange = () => {
api.removeEventListener('onStateChange', onStateChange)
r()
}
api.addEventListener('onStateChange', onStateChange)
})
return api
}
getEls() {
const $player = unsafeWindow.movie_player
const $video = $('video', $player)
const $eventCatcher = $player.parentElement
const $volumeBar = $('.ytp-volume-slider', $player)
return {$video, $eventCatcher, $volumeBar, $player}
}
listenEvents($video) {
const onVolumeChange = this.onVolumeChange.bind(this)
$video.addEventListener('volumechange', onVolumeChange)
addEventListener('unload', () => GM_setValue(this.volumeSk, this.volume))
}
onVolumeChange() {
this.volume = this.api.getVolume()
}
}
class WheelVolume {
constructor(player, api, $volumeBar, $player) {
this.player = player
this.api = api
this.$volumeBar = $volumeBar
this.$player = $player
this.events = {
mouseover: new Event('mouseover', {bubbles: true}),
mouseout: new Event('mouseout', {bubbles: true}),
mousemove: new Event('mousemove')
}
}
init($eventCatcher) {
const onWheel = this.onWheel.bind(this)
const onClick = this.onClick.bind(this)
$eventCatcher.addEventListener('wheel', onWheel)
$eventCatcher.addEventListener('mousedown', onClick)
}
onWheel(e) {
e.preventDefault()
e.stopImmediatePropagation()
this.show()
const now = Date.now(), since = now - this.prevScrollDate
const step = (e.deltaY < 0 ? 1 : -1) * (since < 50 ? 4 : 1)
this.api.setVolume(this.player.volume + step)
this.prevScrollDate = now
}
onClick(e) {
if (e.which != 2) return
e.preventDefault()
this.show()
const api = this.api
if (api.isMuted()) {
api.unMute()
api.setVolume(this.player.volume)
}
else api.mute()
}
show() {
const $volumeBar = this.$volumeBar, events = this.events
this.$player.dispatchEvent(events.mousemove)
clearTimeout(this.showTimeout)
$volumeBar.dispatchEvent(events.mouseover)
this.showTimeout = setTimeout(() => $volumeBar.dispatchEvent(events.mouseout), 1000)
}
}
class RealAutoPlay {
constructor(api) {
this.api = api
this.states = {unstarted: -1, ended: 0, paused: 2}
this.popupName = 'yt-confirm-dialog-renderer'
this.$popupContainer = $('ytd-popup-container')
this.$toggleAutoNavBtn = $('.ytp-autonav-toggle-button')
this.autoNavEnabled = this.$toggleAutoNavBtn.ariaChecked
}
init() {
const onStateChange = this.onStateChange.bind(this)
const onToggleAutoNav = this.onToggleAutoNav.bind(this)
this.api.addEventListener('onStateChange', onStateChange)
this.$toggleAutoNavBtn.addEventListener('click', onToggleAutoNav)
}
onStateChange(state) {
const states = this.states
switch (state) {
case states.ended:
if (this.autoNavEnabled && !document.hasFocus()) this.api.nextVideo()
break
case states.unstarted:
case states.paused:
this.bypassPopup()
}
}
onToggleAutoNav() {
this.autoNavEnabled = this.$toggleAutoNavBtn.ariaChecked
}
bypassPopup() {
const popup = this.$popupContainer.popups_[this.popupName]
if (!popup) return
this.api.playVideo()
popup.popup.remove()
delete this.$popupContainer.popups_[this.popupName]
}
}
const $ = (sel, el = document) => el.querySelector(sel)
const wait = async (ms) => await new Promise(r => setTimeout(r, ms))
new Player().init()