// ==UserScript== // @name 虎牙实验室 // @namespace https://gitee.com/Kaiter-Plus/TampermonkeyScript/tree/master/huya-lab // @author Kaiter-Plus // @description 给虎牙直播添加额外功能(同步时间、画面镜像、自动选择最高画、自动选领取百宝箱奖励、自动网页全屏) // @version 2.0.2 // @license BSD-3-Clause // @match *://*.huya.com/* // @icon https://www.huya.com/favicon.ico // @require https://cdnjs.cloudflare.com/ajax/libs/vue/2.7.13/vue.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/element-ui/2.15.14/index.js // @resource elementStyle https://cdnjs.cloudflare.com/ajax/libs/element-ui/2.15.14/theme-chalk/index.css // @noframes // @grant GM_getResourceText // @grant GM_setValue // @grant GM_getValue // @grant GM_addStyle // @run-at document-end // @downloadURL https://update.greasyfork.icu/scripts/420867/%E8%99%8E%E7%89%99%E5%AE%9E%E9%AA%8C%E5%AE%A4.user.js // @updateURL https://update.greasyfork.icu/scripts/420867/%E8%99%8E%E7%89%99%E5%AE%9E%E9%AA%8C%E5%AE%A4.meta.js // ==/UserScript== ;(function () { 'use strict' // 创建一个容器 const container = document.createElement('div') container.id = 'huya-lab' container.innerHTML = `` document.body.appendChild(container) // 添加 element-ui 样式 const elementStyle = GM_getResourceText('elementStyle') GM_addStyle(elementStyle) // 添加样式 initStyle() // 记录获取播放器控制栏获取失败的次数 let failCount = 0 // 播放器控制栏 let playerControl = null // 等待播放器的控制栏加载完成 const timer = setInterval(() => { // 如果加载失败超过 100 次,提示插件初始化失败 if (failCount >= 100) { clearInterval(timer) Vue.prototype.$message.error('虎牙实验室插件加载失败,请刷新重试~') } else { if (playerControl === null) { failCount += 1 playerControl = document.getElementById('player-ctrl-wrap') } else { clearInterval(timer) // 开始初始化 init() } } }, 1500) function init() { // 创建元素方法 Vue.prototype.createPlayerButton = option => { const btn = document.createElement(option.tagName) btn.id = option.id ? option.id : '' btn.className = option.className ? option.className : '' btn.title = option.title ? option.title : '' btn.innerHTML = option.innerHTML ? option.innerHTML : '' btn.style = option.style ? option.style : '' btn.tabIndex = option.tabIndex ? option.tabIndex : '' if (option.clickEventListener) btn.addEventListener('click', option.clickEventListener) return btn } // 时间同步 const syncTimeIcon = { data() { return { syncCurrentTime: false } }, created() { // 初始化状态 this.syncCurrentTime = GM_getValue('syncCurrentTime') ? true : false this.toggleIcon(this.syncCurrentTime) }, methods: { // 切换同步时间按钮显示和隐藏 toggleIcon(value) { if (!playerControl) { this.$message.warning('虎牙播放器控制条还没有加载完成,请稍后操作~') this.syncCurrentTime = !this.syncCurrentTime } else { if (value) { const icon = this.createIcon() playerControl.appendChild(icon) } else { const syncBtn = document.getElementById('hy-sync') if (syncBtn) syncBtn.parentNode.removeChild(syncBtn) } // 保存状态 GM_setValue('syncCurrentTime', value) } }, // 创建同步时间按钮 createIcon() { const icon = this.createPlayerButton({ tagName: 'div', id: 'hy-sync', className: 'player-ctrl__btn', title: '同步时间', innerHTML: ` `, clickEventListener: this.clickHandler }) return icon }, // 同步时间功能 clickHandler() { const videoNode = document.getElementById('hy-video') if (videoNode.tagName !== 'VIDEO') this.$message.warning('当前直播间不支持使用时间同步功能~') if (videoNode) { const buffered = videoNode.buffered if (buffered.length !== 0) videoNode.currentTime = buffered.end(0) } else { this.$message.warning('虎牙直播视频容器还没有加载出来,请稍后重试~') } } }, template: `
播放器同步时间功能
` } // 镜像 const reversePlayerIcon = { data() { return { reversePlayer: false, isReverse: false } }, created() { // 初始化状态 this.reversePlayer = GM_getValue('reversePlayer') ? true : false this.toggleIcon(this.reversePlayer) }, methods: { // 切换镜像按钮显示和隐藏 toggleIcon(value) { if (!playerControl) { this.$message.warning('虎牙播放器控制条还没有加载完成,请稍后操作~') this.reversePlayer = !this.reversePlayer } else { if (value) { const icon = this.createIcon() playerControl.appendChild(icon) } else { const reverseBtn = document.getElementById('hy-reverse') if (reverseBtn) reverseBtn.parentNode.removeChild(reverseBtn) } // 保存状态 GM_setValue('reversePlayer', value) } }, // 创建镜像按钮 createIcon() { const icon = this.createPlayerButton({ tagName: 'div', id: 'hy-reverse', className: 'player-ctrl__btn', title: '镜像画面', innerHTML: ` `, clickEventListener: this.clickHandler }) return icon }, // 画面镜像功能 clickHandler() { const videoNode = document.getElementById('hy-video') if (videoNode) { videoNode.style.transformOrigin = 'center' if (this.isReverse) { videoNode.style.transform = 'rotateY(0deg)' this.isReverse = false } else { videoNode.style.transform = 'rotateY(180deg)' this.isReverse = true } } else { this.$message.warning('虎牙直播视频容器还没有加载出来,请稍后重试~') } } }, template: `
播放器镜像播放功能
` } // 自动选择最高画质 const selectHightestQuality = { data() { return { autoSelect: false } }, created() { // 初始化状态 this.autoSelect = GM_getValue('selectHightestQuality') ? true : false this.toggleState(this.autoSelect) }, methods: { // 功能初始化 toggleState(value) { if (value) { const hightest = document.querySelector('.player-videotype-list').children[0] console.log(hightest) if (hightest.className === 'on') return hightest.click() // 防止切换画质时视频停止播放 const timer = setInterval(() => { if (document.querySelector('.player-play-btn')) { document.querySelector('.player-play-btn')[0].click() document.querySelector('#player-tip-pause').remove() clearInterval(timer) } }, 500) } // 保存状态 GM_setValue('selectHightestQuality', value) } }, template: `
自动选择最高画质功能
` } // 自动选领取百宝箱 const autoGetChests = { data() { return { enabled: false, timer: null, loaded: false, // 领取页面是否加载完毕 chestList: null // 所有宝箱 } }, created() { // 初始化状态 this.enabled = GM_getValue('autoGetChests') ? true : false this.toggleState(this.enabled) }, methods: { // 功能初始化 toggleState(value) { if (value) { this.loadChestView() // 获取白炽宝箱(普通用户宝箱) if (!document.querySelector('#box-list-top .item:nth-child(1)')) { this.$message.warning('白炽宝箱还没有加载完成,请稍后操作~') this.toggleState(this.enabled) } else { this.getChests() } } else { clearInterval(this.timer) } // 保存状态 GM_setValue('autoGetChests', value) }, // 自动获取百宝箱 getChests() { this.timer = setInterval(() => { if (!this.chestList) { this.chestList = Array.from(document.querySelectorAll('#box-item-list>.box-item')) } else { // 如果已经领取完了,清除定时器 const lastChestText = this.chestList[this.chestList.length - 1].querySelector('p').innerText if (lastChestText === '已领取') { // this.$message.warning('宝箱已经领取完了~') this.chestList = null clearInterval(this.timer) } } // 如果当前是可领取状态,领取 const btn = document.querySelector('#box-list-top .item:nth-child(1) .btn') if (btn.style.display === 'block') { // this.$message.warning('开始领取宝箱了~') btn.click() // 隐藏领取成功弹框 document.querySelector('#player-box-panel .box-mask').style.display = 'none' } }, 10000) }, loadChestView() { if (!this.loaded) { // 第一次加载需要点击一下才会出现页面 const chestView = document.querySelector('#box-list-top') if (!chestView) { const chestBtn = document.querySelector('#player-gift-wrap .player-chest-btn') chestBtn.click() // 点击之后会显示宝箱的视图,隐藏 document.querySelector('#player-box-panel .close').click() } else { this.loaded = true } } } }, template: `
自动选领取百宝箱功能
` } // 网页全屏 const pageFullScreen = { data() { return { enabled: false, dynamicStyle: null // 动态样式 } }, created() { // 初始化状态 this.enabled = GM_getValue('pageFullScreen') ? true : false this.toggleState(this.enabled) }, methods: { // 功能初始化 toggleState(value) { // 保存状态 GM_setValue('pageFullScreen', value) // 如果是 true 则开启网页全屏 if (value) { // 添加全屏动态样式 this.addDynamicStyle() if (!playerControl) { this.$message.warning('虎牙播放器控制条还没有加载完成,请稍后操作~') this.enabled = !this.enabled } else { // 获取 剧场模式 按钮并点击 playerControl.querySelector('#player-fullpage-btn').click() // 获取 右侧聊天框 并点击 const danmuChat = document.querySelector('#player-fullpage-right-btn') if (Array.from(danmuChat.classList).indexOf('player-fullpage-right-open') === -1) { danmuChat.click() } // 隐藏 礼物栏 document.querySelector('#player-gift-wrap').style.display = 'none' // 隐藏 全屏按钮 playerControl.querySelector('#player-fullscreen-btn').style.display = 'none' // 播放器高度设为 100% document.querySelector('#player-wrap').style.height = '100%' // 显示弹幕输入框 playerControl.querySelector('#player-full-input').style.display = 'block' // 隐藏 控制栏 playerControl.classList.add('hidden-controls') // 退出剧场模式事件 this.quitFullScreen() // 添加鼠标事件 this.addEvent() } } }, // 添加样式 addDynamicStyle() { // 添加网页全屏样式 this.dynamicStyle = GM_addStyle(` .hidden-controls { bottom: -44px!important; } .show-controls { bottom: 0px!important; } `) }, // 添加事件 addEvent() { const mouseWrap = document.querySelector('#player-mouse-event-wrap') const danMuInput = playerControl.querySelector('#player-full-input-txt') mouseWrap.addEventListener('mousemove', this.throttle(this.showControls, 300)) mouseWrap.addEventListener('mousemove', this.debounce(this.hiddenControls, 1200)) // 1.2s 后隐藏控制栏 playerControl.addEventListener('mouseover', this.showControls, true) // 回车显示并聚焦输入框 document.addEventListener('keydown', e => { if (e.key.toLowerCase() === 'enter') { this.showControls() danMuInput.focus() } }) // 回车发送弹幕 danMuInput.addEventListener('keydown', e => { e.stopPropagation() if (e.key.toLowerCase() === 'enter') { playerControl.querySelector('#player-full-input-btn').click() document.querySelector('#ex-settings').focus() this.hiddenControls() } }) }, // 退出网页全屏 quitFullScreen() { const narrowPageBtn = playerControl.querySelector('.player-narrowpage') narrowPageBtn.onclick = () => { this.enabled = !this.enabled GM_setValue('pageFullScreen', this.enabled) if (!this.enabled) { // 移除网页全屏样式 this.dynamicStyle.parentNode.removeChild(this.dynamicStyle) narrowPageBtn.onclick = null // 显示 全屏按钮 playerControl.querySelector('#player-fullscreen-btn').style.display = 'block' } } }, // 显示 控制栏 showControls() { playerControl.classList.add('show-controls') playerControl.classList.remove('hidden-controls') }, // 隐藏 控制栏 hiddenControls() { playerControl.classList.remove('show-controls') playerControl.classList.add('hidden-controls') }, // 节流函数 throttle(fn, delay) { let valid = true return function () { if (!valid) { return false } valid = false setTimeout(() => { fn() valid = true }, delay) } }, // 防抖函数 debounce(fn, delay) { let timer = null return function () { if (timer) { clearTimeout(timer) } timer = setTimeout(fn, delay) } } }, template: `
自动网页全屏功能
` } // 实验室 const lab = { components: { syncTimeIcon, reversePlayerIcon, selectHightestQuality, autoGetChests, pageFullScreen }, data() { return { lab: { pageFullScreen: false } } }, created() { this.removeAdvertisement() }, methods: { // 移除虎牙自带的广告 removeAdvertisement() { let advertisement = document.querySelector('.duya-header-ad') const timer = setInterval(() => { if (advertisement) { advertisement.parentNode.removeChild(advertisement) clearInterval(timer) } else { advertisement = document.querySelector('.duya-header-ad') } }) } }, template: `
实验室
` } // 创建 vue 实例 const app = new Vue({ el: container, components: { 'hy-lab-icon': lab } }) } // 添加自定义样式 function initStyle() { GM_addStyle(` @font-face { font-family: element-icons; src:url(https://unpkg.com/element-ui/lib/theme-chalk/fonts/element-icons.woff) format("woff"); } #huya-lab { position: fixed; right: 0; bottom: 20%; padding: 12px 4px; border: 1px solid #e3e5e7; border-radius: 12px 0 0 12px; background: #fff; z-index: 10; transform: translate3d(0,0,0); transition: height cubic-bezier(.22,.58,.12,.98) .4s; box-shadow: 0 0 20px 0 rgba(0, 85, 255, 0.1); } .hy-lab-icon { display: flex; flex-direction: column; justify-content: center; align-items: center; margin: 4px 0; padding: 5px 4px; color: #ff9600; cursor: pointer; } .hy-lab-icon:hover .icon { animation: jumping cubic-bezier(.22,.58,.12,.98) 1.5s infinite; } @keyframes jumping { 0% { transform: translate(0,0) scale(1); } 10% { transform: translate(0,4px) scale(.8); } 30% { transform: translate(0,-13px) scale(.95); } 45% { transform: translate(0,4px) scale(.8); } 50% { transform: translate(0,0) scale(1); } 53% { transform: translate(0,0) scale(.9); } 55% { transform: translate(0,0) scale(1); } 57% { transform: translate(0,0) scale(.9); } 61% { transform: translate(0,0) scale(1); } 100% { transform: translate(0,0) scale(1); } } .lab-item { display: flex; justify-content: space-between; align-items: center; color: #ff9600; height: 40px; } .lab-item__title { font-size: 16px; line-height: 24px; } /* 播放器控制栏图标样式 */ .player-ctrl__btn { display: block; position: relative; float: left; width: 24px; height: 24px; margin: 10px 0 10px 15px; cursor: pointer; } .player-ctrl__btn .icon { fill: currentColor; cursor: pointer; color: #b2b4b4; } .player-ctrl__btn:hover .icon { fill: currentColor; color: #ff9600; } `) } })()