Greasy Fork is available in English.
增强B站功能,支持视频合集倒序播放,还有一些其他小功能
当前为
// ==UserScript==
// @name B站合集倒序播放
// @description 增强B站功能,支持视频合集倒序播放,还有一些其他小功能
// @version 0.0.3
// @author Grant Howard, Coulomb-G
// @copyright 2024, Grant Howard
// @license MIT
// @match *://*.bilibili.com/video/*
// @exclude *://api.bilibili.com/*
// @exclude *://api.*.bilibili.com/*
// @exclude *://*.bilibili.com/api/*
// @exclude *://member.bilibili.com/studio/bs-editor/*
// @exclude *://t.bilibili.com/h5/dynamic/specification
// @exclude *://bbq.bilibili.com/*
// @exclude *://message.bilibili.com/pages/nav/header_sync
// @exclude *://s1.hdslb.com/bfs/seed/jinkela/short/cols/iframe.html
// @exclude *://open-live.bilibili.com/*
// @run-at document-start
// @grant unsafeWindow
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_deleteValue
// @grant GM_info
// @grant GM_xmlhttpRequest
// @grant GM_registerMenuCommand
// @grant GM_unregisterMenuCommand
// @grant GM_addStyle
// @connect raw.githubusercontent.com
// @connect github.com
// @connect cdn.jsdelivr.net
// @connect cn.bing.com
// @connect www.bing.com
// @connect translate.google.cn
// @connect translate.google.com
// @connect localhost
// @connect *
// @icon https://cdn.jsdelivr.net/gh/the1812/Bilibili-Evolved@preview/images/logo-small.png
// @icon64 https://cdn.jsdelivr.net/gh/the1812/Bilibili-Evolved@preview/images/logo.png
// @namespace http://greasyfork.icu/users/734541
// ==/UserScript==
;(() => {
GM_addStyle(`.qmsg.qmsg-wrapper {
position: fixed;
top: calc(50vh - (53px));
left: 0;
z-index: 1010;
width: 100%;
pointer-events: none;
color: rgba(0, 0, 0, 0.55);
font-size: 13px;
font-variant: tabular-nums;
font-feature-settings: 'tnum';
}
.qmsg .qmsg-item {
padding: 8px;
text-align: center;
animation-duration: 0.3s;
}
.qmsg .qmsg-item .qmsg-content {
text-align: left;
position: relative;
display: inline-block;
padding: 10px 12px;
background: #fff;
border-radius: 4px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
pointer-events: all;
max-width: 80%;
min-width: 80px;
}
.qmsg .qmsg-item .qmsg-content [class^='qmsg-content-'] {
display: flex;
align-items: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.qmsg .qmsg-item .qmsg-content [class^='qmsg-content-'] .qmsg-icon {
display: inline-block;
height: 16px;
}
.qmsg .qmsg-item .qmsg-content [class^='qmsg-content-'] .qmsg-icon:first-child {
margin-right: 8px;
}
.qmsg .qmsg-item .qmsg-content [class^='qmsg-content-'] .qmsg-icon-close {
cursor: pointer;
color: rgba(0, 0, 0, 0.45);
transition: color 0.3s;
margin-left: 6px;
}
.qmsg .qmsg-item .qmsg-content [class^='qmsg-content-'] .qmsg-icon-close:hover > svg path {
stroke: #555;
}
.qmsg .qmsg-item .qmsg-content [class^='qmsg-content-'] .qmsg-count {
display: inline-block;
position: absolute;
left: -8px;
top: -8px;
color: #fff;
font-size: 12px;
text-align: center;
height: 16px;
line-height: 16px;
border-radius: 3px;
min-width: 16px;
animation-duration: 0.3s;
}
.qmsg .qmsg-item .qmsg-content-info {
color: #909399;
}
.qmsg .qmsg-item .qmsg-content-info .qmsg-count {
background-color: #909399;
}
.qmsg .qmsg-item .qmsg-content-warning {
color: #e6a23c;
}
.qmsg .qmsg-item .qmsg-content-warning .qmsg-count {
background-color: #e6a23c;
}
.qmsg .qmsg-item .qmsg-content-error {
color: #f56c6c;
}
.qmsg .qmsg-item .qmsg-content-error .qmsg-count {
background-color: #f56c6c;
}
.qmsg .qmsg-item .qmsg-content-success {
color: #67c23a;
}
.qmsg .qmsg-item .qmsg-content-success .qmsg-count {
background-color: #67c23a;
}
.qmsg .qmsg-item .qmsg-content-loading {
color: #409eff;
}
.qmsg .qmsg-item .qmsg-content-loading .qmsg-count {
background-color: #409eff;
}
.qmsg .animate-turn {
animation: MessageTurn 1s linear infinite;
}
@keyframes MessageTurn {
0% {
transform: rotate(0deg);
}
25% {
transform: rotate(90deg);
}
50% {
transform: rotate(180deg);
}
75% {
transform: rotate(270deg);
}
100% {
transform: rotate(360deg);
}
}
@keyframes MessageMoveOut {
0% {
max-height: 150px;
padding: 8px;
opacity: 1;
}
to {
max-height: 0;
padding: 0;
opacity: 0;
}
}
@keyframes MessageMoveIn {
0% {
transform: translateY(-100%);
transform-origin: 0 0;
opacity: 0;
}
to {
transform: translateY(0);
transform-origin: 0 0;
opacity: 1;
}
}
@keyframes MessageShake {
0%,
100% {
transform: translateX(0px);
opacity: 1;
}
25%,
75% {
transform: translateX(-4px);
opacity: 0.75;
}
50% {
transform: translateX(4px);
opacity: 0.25;
}
}`)
GM_addStyle(`#zaizai-div .video-sections-head_second-line {
display: flex;
flex-wrap: wrap;
align-items: center;
margin: 12px 16px 0;
color: var(--text3);
color: var(--text3);
padding-bottom: 12px;
font-size: 14px;
line-height: 16px;
gap: 10px 20px;
}
#zaizai-div .border-bottom-line {
height: 1px;
background: var(--line_regular);
margin: 0 15px;
}
#zaizai-div .switch-button {
margin: 0;
display: inline-block;
position: relative;
width: 30px;
height: 20px;
border: 1px solid #ccc;
outline: none;
border-radius: 10px;
box-sizing: border-box;
background: #ccc;
cursor: pointer;
transition: border-color .2s, background-color .2s;
vertical-align: middle;
}
#zaizai-div .switch-button.on:after {
left: 11px;
}
#zaizai-div .switch-button:after {
content: "";
position: absolute;
top: 1px;
left: 1px;
border-radius: 100%;
width: 16px;
height: 16px;
background-color: #fff;
transition: all .2s;
}
#zaizai-div .switch-button.on {
border: 1px solid var(--brand_blue);
background-color: var(--brand_blue);
}
#zaizai-div .txt {
margin-right: 4px;
vertical-align: middle;
}
`)
const console = (() => {
const _console = window.console
return {
log: (...args) => {
_console.log(`%c ZAIZAI `, 'padding: 2px 1px; border-radius: 3px; color: #fff; background: #42c02e; font-weight: bold;', ...args)
}
}
})()
// 全局变量
const local = useReactiveLocalStorage({
defaultreverseorder: false,
// 开启倒序播放
startreverseorder: false,
addsectionslistheigth: false
})
let Video = null
let videoSections = null
let keyupCodeFn = {}
function useReactiveLocalStorage(obj) {
let data = {}
let zaizaiStore = window.localStorage.getItem('zaizai-store')
if (zaizaiStore) {
zaizaiStore = JSON.parse(zaizaiStore)
for (const key in obj) {
data[key] = zaizaiStore[key] || obj[key]
}
} else {
data = obj
}
let handler = {
set(target, key, value) {
let res = Reflect.set(target, key, value)
try {
window.localStorage.setItem(`zaizai-store`, JSON.stringify(data))
} catch (error) {
console.log('存储失败,请检查浏览器设置', error)
}
return res
},
get(target, key) {
let ret = Reflect.get(target, key)
return typeof ret === 'object' ? new Proxy(ret, handler) : ret
}
}
data = new Proxy(data, handler)
return data
}
function waitTime(callback, options = { time: 500, isSetup: false }) {
let timeout = null
return new Promise(resolve => {
if (options.isSetup) {
let res = callback()
if (res) resolve(res)
}
timeout = setInterval(() => {
let res = callback()
if (res) {
clearInterval(timeout)
resolve(res)
}
}, options.time)
})
}
async function selectVideo() {
await waitTime(
() => {
let video = document.querySelector('video')
if (video) {
Video = video
return true
}
},
{
isSetup: true
}
)
}
async function switchAddsectionslistheigthOnClick(action) {
if (typeof action !== 'string') {
local.addsectionslistheigth = !local.addsectionslistheigth
}
const ListEl = await waitTime(
() => {
return document.querySelector('.video-sections-content-list')
},
{ isSetup: true }
)
if (local.addsectionslistheigth) {
typeof action !== 'string' && this.classList.add('on')
ListEl.style.maxHeight = '40vh'
ListEl.style.height = '40vh'
} else {
typeof action !== 'string' && this.classList.remove('on')
ListEl.style.height = '150px'
ListEl.style.maxHeight = '150px'
}
}
async function getisReverseorder() {
const playerloop_checkbox = await waitTime(
() => {
let checkbo = document.querySelector(`.bui-switch-input[aria-label="洗脑循环"]`)
if (checkbo) {
return checkbo
}
},
{ isSetup: true }
)
return playerloop_checkbox
}
async function bindWatch() {
const playerloop_checkbox = await getisReverseorder()
playerloop_checkbox.addEventListener('change', () => {
if (playerloop_checkbox.checked) {
local.startreverseorder = false
document.querySelector('#startreverseorder').classList.remove('on')
Video.removeEventListener('ended', VideoOnEnded)
}
})
}
async function VideoOnPlay() {
// local.startreverseorder &&
if (!document.querySelector('#zaizai-div')) {
const div = document.createElement('div')
div.id = 'zaizai-div'
let isstartreverseorder = await getisReverseorder()
div.innerHTML = `
<div class="video-sections-head">
<div class="border-bottom-line"></div>
<div class="video-sections-head_second-line">
<div>
<span class="txt">默认开启倒序播放</span>
<span id="defaultreverseorder" class="switch-button ${local.defaultreverseorder ? 'on' : ''}"></span>
</div>
<div>
<span class="txt">倒序播放</span>
<span id="startreverseorder" class="switch-button ${isstartreverseorder.checked ? 'on' : ''}"></span>
</div>
<div>
<span class="txt">增高合集列表</span>
<span id="addsectionslistheigth" class="switch-button ${local.addsectionslistheigth ? 'on' : ''}"></span>
</div>
</div>
</div>
`
videoSections.appendChild(div)
// 默认开启倒序播放
let defaultreverseorder = document.querySelector('#defaultreverseorder')
function defaultSwitchClick() {
local.defaultreverseorder = !local.defaultreverseorder
if (local.defaultreverseorder) {
this.classList.add('on')
} else {
this.classList.remove('on')
}
}
defaultreverseorder.addEventListener('click', defaultSwitchClick)
// 倒序播放
let startreverseorder = document.querySelector('#startreverseorder')
async function switchReverseoOnClick() {
const playerloop_checkbox = await getisReverseorder()
if (playerloop_checkbox.checked) {
Qmsg && Qmsg.warning('请关闭"洗脑循环"后再开启倒序播放')
return
}
local.startreverseorder = !local.startreverseorder
if (local.startreverseorder) {
startreverseorder.classList.add('on')
Video.addEventListener('ended', VideoOnEnded)
} else {
startreverseorder.classList.remove('on')
Video.removeEventListener('ended', VideoOnEnded)
}
}
startreverseorder.addEventListener('click', switchReverseoOnClick)
const button = document.querySelector('.video-sections-head_second-line button').cloneNode()
button.textContent = '滚动到当前播放'
button.style.width = '100%'
function scrollToCurrent() {
let { currentEl } = getCurrentcard()
const sectionsListEl = document.querySelector('.video-sections-content-list')
// 42 = currentEl.clientHeight + margin 4 = 列表第一个有4px的margin-top 12是自定义
let scrollToPosition = currentEl.offsetTop - sectionsListEl.clientHeight / 2 - 42 - 4 - 12
sectionsListEl.scrollTo({
top: scrollToPosition
})
}
button.addEventListener('click', scrollToCurrent)
const newdiv = document.createElement('div')
newdiv.style.width = '100%'
newdiv.appendChild(button)
div.querySelector('.video-sections-head_second-line').appendChild(newdiv)
let addsectionslistheigth = document.querySelector('#addsectionslistheigth')
addsectionslistheigth.addEventListener('click', switchAddsectionslistheigthOnClick)
}
}
function getCurrentcard() {
const episodecards = document.querySelectorAll('.video-episode-card')
let i = 0
for (const element of episodecards) {
let curicon = element.querySelector('.cur-play-icon')
if (curicon.style.display !== 'none') {
break
}
i++
}
// 顺序上一个
let previous = i - 1 <= 0 ? episodecards.length - 1 : i - 1
// 顺序下一个
let next = i + 1 >= episodecards.length - 1 ? episodecards.length - 1 : i + 1
return {
elements: episodecards,
current: i,
currentEl: episodecards[i],
next,
nextEl: episodecards[next],
previous,
previousEl: episodecards[previous]
}
}
function VideoOnEnded() {
/* let curpage = document.querySelector('.cur-page').textContent
curpage = curpage.match(/\d+/g).at(-1)
curpage = parseInt(curpage) */
const { previousEl } = getCurrentcard()
previousEl.click()
}
function keyup_key_g() {
document.querySelector(`.bpx-player-ctrl-btn[aria-label="网页全屏"]`).click()
}
keyupCodeFn['g'] = keyup_key_g
function keyup_key_h() {
document.querySelector(`.bpx-player-ctrl-btn[aria-label="画中画"]`).click()
}
keyupCodeFn['h'] = keyup_key_h
async function main() {
console.log('mian start')
await waitTime(() => {
let progress = document.querySelector('.bpx-player-progress-schedule-current')
if (progress) {
let transform = progress.style.transform.replace('scaleX(', '').replace(')', '')
if (transform > 0) {
videoSections = document.querySelector('.base-video-sections-v1')
if (!videoSections) {
videoSections = document.querySelector('.video-sections-v1')
}
return true
}
}
})
if (!videoSections) {
console.log('mian stop 没有合集')
return
}
await selectVideo()
Video.addEventListener('play', VideoOnPlay)
if (local.defaultreverseorder) {
Video.addEventListener('ended', VideoOnEnded)
}
await switchAddsectionslistheigthOnClick('0')
await VideoOnPlay()
await bindWatch()
window.addEventListener('keyup', e => {
console.log('keyup', e)
keyupCodeFn[e.key] && keyupCodeFn[e.key]()
})
console.log('mian stop 成功开启')
}
window.onload = async () => {
console.log('正式-v3')
let addscript = new Promise(resolve => {
const script = document.createElement('script')
script.src = 'https://cdn.jsdelivr.net/gh/yaohaixiao/message.js/message.min.js'
script.onload = () => {
resolve()
}
document.body.appendChild(script)
})
try {
await addscript
} catch (e) {
console.log('添加 message 失败')
}
console.log('unsafeWindow', unsafeWindow.Qmsg)
main().catch(err => {
console.log(err)
})
}
})()