视频内显示分P信息(方便全屏时查看)
// ==UserScript== // @name [Bilibili] 视频内显工具 // @namespace ckylin-script-bilibili-shownameinside // @version 1.5.1 // @description 视频内显示分P信息(方便全屏时查看) // @author CKylinMC // @match https://*.bilibili.com/* // @exclude https://www.bilibili.com/bangumi/play* // @grant unsafeWindow // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @grant GM_unregisterMenuCommand // @run-at document-body // @require https://greasyfork.org/scripts/429720-cktools/code/CKTools.js?version=1023553 // @license GPLv3 // ==/UserScript== (async function(){ 'use strict'; const instance = Math.floor(Math.random()*100000); class Logger{ constructor(prefix='[logUtil]'){ this.prefix = prefix; } log(...args){ console.log(this.prefix,...args); } info(...args){ console.info(this.prefix,...args); } warn(...args){ console.warn(this.prefix,...args); } error(...args){ console.error(this.prefix,...args); } } const defaultList = ["设置按钮","分P编号","斜杠","分P数量","间隔符","分P标题"]; const logger = new Logger("[SNI "+instance+"]"); if(CKTools.ver<1.2){ logger.warn("Library script 'CKTools' was loaded incompatible version "+CKTools.ver+", so that SNI may couldn't work correctly. Please consider update your scripts."); } const {get,getAll,domHelper,wait,waitForDom,waitForPageVisible,addStyle,modal,bili} = CKTools; function getContainer(clear=false){ let dom = get("#ck-sni-container"); if(!dom) dom = domHelper("div",{ id: "ck-sni-container", append: get("div.bilibili-player-video-wrap, .bpx-player-video-wrap") }); else if(dom.getAttribute("data-sni-instance")!=instance+"") { logger.error("Multi instance running! An error throwed by this."); throw new Error("Multi instance running!"); } dom.setAttribute("data-sni-instance",instance+""); if(clear) dom.innerHTML = ""; return dom; } const Modules = { "设置按钮": d=>domHelper('span',{ classnames:['ck-sni-clickable'], text: "🛠️", listeners:{ click: openSettingsModal } }), "间隔符": d=>" | ", "空白": d=>" ", "斜杠": d=>" / ", "分P标题": d=>{ const parts = d.info.vid.pages; const findpart = parts.filter(page=>page.cid==+d.info.cid); if(findpart.length){ const part = findpart[0]; return part.part; }else return null; }, "分P编号": d=>{ const parts = d.info.vid.pages; const total = parts.length; const findpart = parts.filter(page=>page.cid==+d.info.cid); if(findpart.length){ const part = findpart[0]; return part.page; }else return null; }, "分P数量": d=>d.info.vid.videos, "BV号": d=>d.info.bvid, "AV号": d=>d.info.aid, "标题": d=>d.info.vid.title, "分区": d=>d.info.vid.tname, "UP主": d=>d.info.vid.owner.name, "简介": d=>d.info.vid.desc, "弹性空白": d=>domHelper('span',{ css:{ flex: 2, }, }), }; const running = {}; function debounce(func, timeout = 300) { let timer; return (...args) => { clearTimeout(timer); timer = setTimeout(() => { func.apply(this, args); }, timeout); }; } const MenuManager = { ids:[], menus:{}, registerMenu: (text, callback) => MenuManager.ids.push(GM_registerMenuCommand(text, callback)), clearMenu: () => {MenuManager.ids.forEach(id => GM_unregisterMenuCommand(id)); MenuManager.ids = [];}, setMenu:(id,text,callback,noapply = false)=>{ MenuManager.menus[id] = { text, callback }; if (!noapply) MenuManager.applyMenus(); }, applyMenus:()=>{ MenuManager.clearMenu(); for (let item in MenuManager.menus) { if(!MenuManager.menus.hasOwnProperty(item)) continue; let menu = MenuManager.menus[item]; MenuManager.registerMenu(menu.text, menu.callback); } } }; function getValueOrDefault(key,fallback=null){ const val = GM_getValue(key); return typeof val === 'undefined' ? fallback : val; } function saveValue(k,v){ return GM_setValue(k,v); } function getModule(name){ if(Modules.hasOwnProperty(name)){ return Modules[name]; }else{ return undefined; } } async function runModulesFromList(wrapper,list,...args){ for(const name of list){ logger.info("Executing module",name); try{ const mod = getModule(name); //logger.info("Got module",mod); if(mod){ let r###lt; if(mod.constructor.name=='AsyncFunction'){ r###lt = await mod(...args); }else { r###lt = mod(...args); } if(r###lt){ if(r###lt instanceof HTMLElement||r###lt instanceof Node){ wrapper.appendChild(r###lt); logger.log(name,"dom",r###lt); }else if(r###lt instanceof String || typeof r###lt=='string' || typeof r###lt=='number'){ wrapper.appendChild(document.createTextNode(r###lt)); logger.log(name,"text",r###lt); }else{ logger.log(name,"unknownr###lt","skipped",r###lt); } }else{ logger.log(name,"nor###lt","skipped"); } }else{ //logger.log(name,"nomod","skipped"); wrapper.appendChild(document.createTextNode(name)); logger.log(name,"nameonly",name); } }catch(e){ logger.error(e) }; } } function combineExternalModules(){ if(unsafeWindow.SNIMODULES){ for(const name of Object.keys(unsafeWindow.SNIMODULES)){ const mod = unsafeWindow.SNIMODULES[name]; if(typeof mod ==='function'){ Modules[name] = mod; } } } } function getRunning(k){ return running[k]; } function getCid(){ return unsafeWindow.cid; } function setAnimClass(){ const target = get(".bilibili-player-area"); if(!target) return logger.warn('[anim] Could\'t catch the target.'); target.classList.toggle('ck-sni-animation',getValueOrDefault("enableAnim",false)); } async function inject(){ if(unsafeWindow.player?.getManifest?.()??false){ //remap all variables to global [maybe break other plugins or die in the future] const manifest = unsafeWindow.player.getManifest(); for(let key of Object.keys(manifest)){ unsafeWindow[key] = manifest[key] } } logger.log("injecting - fetching"); saveState(); combineExternalModules(); let info = await bili.getInfoByBvid(unsafeWindow.bvid); if(info&&info.code===0) running.info = info.data; else return logger.error("injecting - info fetch errored"); logger.log("injecting - fetch info ok",info); setAnimClass(); const container = getContainer(true); const list = getValueOrDefault("moduleseq",defaultList); if(list.length === 0) return logger.warn("injecting - exited due to no active module.");; logger.log("injecting - mod list ok",list); const wrapper = domHelper('span',{ id: "ck-sni-wrapper", append: container, init: wrapper=>{ if(getValueOrDefault('enableFlex',false)){ wrapper.style.display = "flex"; domHelper('style',{ append: wrapper, init:style=>{ style.setAttribute("scoped",true); style.appendChild(document.createTextNode(`span{float:none!important;transform:none!important;}`)) } }) } } }); const currentInfo = { info:{ bvid: unsafeWindow.bvid, cid: unsafeWindow.cid, aid: unsafeWindow.aid, vid: running.info }, tools:CKTools,// pass tools into modules for extend use. logger: new Logger("[SNI "+instance+"/module]") }; logger.log("injecting - executing"); getContainer();// call for instance check await runModulesFromList(wrapper, list, currentInfo); logger.log("injecting - done"); } function setLoading(){ const container = getContainer(true); container.innerText = "正在加载..."; } async function regChangeHandler(){ if(running.observer) running.observer.disconnect(); running.observer = new MutationObserver(debounce(e=>{ if(getRunning('cid')!=getCid()){ setLoading(); saveState(); logger.log("Video changes detected"); inject(); } })); let retries = 5; while(retries--){ if(await waitForDom("#bilibili-player")){ running.observer.observe(get("#bilibili-player"),{attributes: true, childList: true, subtree: true}); logger.log("Observer started"); return; }else{ logger.warn("Observer waiting for dom..."); await wait(100); } } logger.warn("Observer not registered correctly."); } function regIntervalStateChangeListener(){ if(running.hrefinterval) clearInterval(running.hrefinterval); running.hrefinterval = setInterval(()=>{ if(isStateChanged()){ setLoading(); saveState(); logger.log("Video changes detected"); inject(); } },1000); } function saveState(){ running.cid = unsafeWindow.cid; running.href = location.href; let p = get("video,bwp-video"); running.blob = p?p.src:null; } function isStateChanged(){ let p = get("video,bwp-video"); let blob = p?p.src:null; return running.cid!=unsafeWindow.cid||running.href!=location.href||running.blob!=blob; } async function openSettingsModal(){ combineExternalModules(); const makeBadge = (name,title="点击移除",click=()=>{})=>domHelper('div',{ classnames: ['ck-sni-mod','ck-sni-grid-item'], css:{ display: "inline-block", margin: "2px", padding: "2px", background: "white", color: "black", borderRadius: "5px", border: "2px solid gray", cursor: "pointer" }, text: name, listeners:{ click:click }, init:el=>{ el.setAttribute("data-sni-mod",name); el.title = title; } }) const makeOption = (options)=>{ const opt = Object.assign({ name: 'opt', optionText: '', description: '选项描述', state:{ enabled: '🟢已启用', disabled: '🔴已禁用' }, initState: false },options); return domHelper('div',{ classnames:['ck-sni-option-wrapper'], childs:[ domHelper('div',{ id: 'ck-sni-option-'+opt.name, css:{ fontWeight: "bold" }, init: optdiv=>{ let getState = ()=>optdiv.getAttribute('enabled')=='yes'; const applyState = state=>{ optdiv.setAttribute('enabled',state?'yes':'no'); optdiv.innerText = (state?opt.state.enabled:opt.state.disabled)+opt.optionText; }; optdiv.onclick = e=>applyState(!getState()); applyState(opt.initState); } }), domHelper('div',{ css:{ paddingLeft: "15px" }, text: opt.description }) ] }); }; const saveOption = (name,configKey)=>{ const optel = get("#ck-sni-option-"+name); if(optel){ let opt = optel.getAttribute("enabled")=='yes'; saveValue(configKey,opt); } } return new Promise(r=>modal.openModal('视频内显设置',domHelper('div',{ id: 'ck-sni-settings', css:{ display: "block" }, childs: [ domHelper('h3',{ css:{ fontWeight: "bold" }, text: '选项' }), makeOption({ name: 'enable-flex', optionText: '弹性布局', description: '开启后允许使用弹性空白,并自动禁用所有浮动和偏移。请注意关闭后弹性空白自动失效。', initState: getValueOrDefault('enableFlex',false) }), makeOption({ name: 'enable-anim', optionText: '额外控件动画', description: '为顶部条和B站底部控制栏添加额外的显隐动画。', initState: getValueOrDefault('enableAnim',false) }), domHelper('h3',{ css:{ fontWeight: "bold" }, text: '组件' }), domHelper('div',{ css:{ fontWeight: "bold" }, text: '已启用:' }), domHelper('div',{ id: "ck-sni-enabled-mods", css:{ margin: "12px", background: "#00ffe740", borderRadius: "5px", padding: "6px" }, init: div=>{ div.classList.add('ck-sni-draggables'); const list = getValueOrDefault("moduleseq",defaultList); for(const name of list){ div.appendChild(makeBadge(name,"点击移除",e=>e.target.remove())); } setTimeout(()=>{ const draggable = new Draggable({ element: document.querySelector('.ck-sni-draggables'), cloneElementClassName: 'ck-sni-clone-grid-item' }); },100); } }), domHelper('div',{ css:{ fontWeight: "bold" }, text: '可添加:' }), domHelper('div',{ id: "ck-sni-available-mods", css:{ margin: "12px", background: "#3e70ff75", borderRadius: "5px", padding: "6px", flexWrap: 'wrap', display: 'flex', maxWidth: '80vw' }, init: div=>{ const list = Object.keys(Modules); for(const name of list){ div.appendChild(makeBadge(name,"点击添加",e=>{ const enabledList = get("#ck-sni-enabled-mods"); if(!enabledList) return; enabledList.appendChild(makeBadge(name,"点击移除",e=>e.target.remove())) })); } } }), domHelper('div',{ css:{ fontWeight: "bold" }, text: '自定义文本:' }), domHelper('div',{ text: '添加自定义纯文本到显示行。请注意,不要与现有模块重名。' }), domHelper('input',{ id: "ck-sni-custom-mods-input", css:{ margin: "12px", background: "#3e70ff75", borderRadius: "5px", padding: "6px", border: "2px solid gray" }, init: input=>{ input.setAttribute('placeholder',"输入自定义纯文本,回车添加"); input.onkeyup = e=>{ if(e.key=="Enter"||e.code=="Enter"||e.keyCode===13){ let val = e.target.value; if(val&&val.trim().length>0){ const enabledList = get("#ck-sni-enabled-mods"); if(!enabledList) return; enabledList.appendChild(makeBadge(val.trim(),"点击移除",e=>e.target.remove())); e.target.value = ''; } } } } }), domHelper('br'), domHelper('button',{ classnames: 'CKTOOLS-toolbar-btns', text: "保存", listeners:{ click: e=>{ const enabledList = [...getAll("#ck-sni-enabled-mods .ck-sni-mod")]; if(!enabledList) return alert("保存失败,列表失效"); const mods = enabledList.map(el=>el.getAttribute("data-sni-mod")); logger.log(enabledList,mods); saveValue("moduleseq",mods); saveOption('enable-flex','enableFlex'); saveOption('enable-anim','enableAnim'); modal.closeModal(); inject(); } } }), domHelper('button',{ classnames: 'CKTOOLS-toolbar-btns', text: "取消", listeners:{ click: e=>{ modal.closeModal(); } } }) ] }))); } async function playerReady() { let i = 150; while (--i > 0) { await wait(100); if (unsafeWindow.player?.isInitialized()??false) break; } if (i < 0) return false; await waitForPageVisible(); while (1) { await wait(200); if (document.querySelector(".bilibili-player-video-control-wrap, .bpx-player-control-wrap")) return true; } } async function startInject(){ //logger.info("Start Trace:", (new Error).stack); if(unsafeWindow.SNI_started){ logger.warn("Someone called start twice. Aborting..."); logger.warn("Trace:", (new Error).stack); return; } unsafeWindow.SNI_started = true; logger.info("waiting for player to be ready"); await waitForPageVisible(); await playerReady(); addStyle(` .bpx-player-top-left{ display:none !important; } #ck-sni-container { pointer-events: none; position: absolute; top: 0; left: 0; background: -moz-linear-gradient(bottom, rgba(0,0,0,0.55) 0%, rgba(0,0,0,0) 100%); background: -webkit-linear-gradient(bottom, rgba(0,0,0,0.55) 0%, rgba(0,0,0,0) 100%); background: linear-gradient(to bottom, rgba(0,0,0,0.55) 0%, rgba(0,0,0,0) 100%); width: 100%; display: block; height: 60px; padding: 8px 0; opacity: 0; z-index: 9999; transition: opacity .3s; } .ck-sni-animation #ck-sni-container{ transition: opacity .3s, transform .2s cubic-bezier(0.74, 0.01, 1, 1), all .3s ease-out !important; transform: translateY(-10px) !important; } .video-control-show #ck-sni-container, .bpx-player-container #ck-sni-container{ transition: opacity .3s; opacity: 1; } .bpx-player-container.bpx-state-no-cursor #ck-sni-container{ transition: opacity .3s; opacity: 0!important; } .ck-sni-animation.video-control-show #ck-sni-container{ transition: opacity .3s, transform .2s cubic-bezier(0.74, 0.01, 1, 1), all .3s ease-out!important; transform: translateY(0px)!important; } .ck-sni-animation.video-control-show .bilibili-player-video-control{ transform: translateY(0px); transition: transform .2s cubic-bezier(0, 0, 0.2, 0.97), all .3s ease-out!important; } .ck-sni-animation .bilibili-player-video-control{ transform: translateY(10px); transition: transform .2s cubic-bezier(0.74, 0.01, 1, 1), all .3s ease-out!important; } #ck-sni-container>#ck-sni-wrapper{ padding: 0 15px; } .video-control-show #ck-sni-container .ck-sni-clickable{ pointer-events: auto !important; } .bpx-player-container #ck-sni-container .ck-sni-clickable{ pointer-events: none !important; } .bpx-player-container.bpx-state-no-cursor #ck-sni-container .ck-sni-clickable{ pointer-events: auto !important; } #ck-sni-container:empty{ display: none; } #ck-sni-container #ck-sni-wrapper{ color: white; background: transparent; } .bilibili-player-video-top { display:none !important; opacity: 0 !important; pointer-events: none !important; } /* Copied from https://juejin.cn/post/7022824391163510821 */ .ck-sni-draggables * { margin: 0; padding: 0; box-sizing: border-box; } .ck-sni-draggables .ck-sni-grid { display: flex; flex-wrap: wrap; margin: 0 -15px -15px 0; touch-action: none; user-select: none; } .ck-sni-draggables .ck-sni-grid-item, .ck-sni-draggables .ck-sni-grid-item * { -moz-user-select:none; -webkit-user-select:none; -ms-user-select:none; user-select:none; } .ck-sni-draggables .active { background: #c8ebfb; } .ck-sni-draggables .ck-sni-clone-grid-item { display: flex; flex-wrap: wrap; margin: 0 -15px -15px 0; touch-action: none; user-select: none; border: 1px solid #d6d6d6; opacity: 0.8; list-style: none; -moz-user-select:none; -webkit-user-select:none; -ms-user-select:none; user-select:none; } `,"ck-sni-styles","unique"); if(get("#ck-sni-identifier")){ logger.warn(instance,"Someone called start twice. Aborting..."); logger.warn(instance,"Trace:", (new Error).stack); return; } domHelper('span',{id:'ck-sni-identifier',append:document.body}); logger.info("start inject"); setLoading(); await inject(); await regChangeHandler(); regIntervalStateChangeListener(); logger.info("loaded"); } /* Copied from https://juejin.cn/post/7022824391163510821 */ class Draggable { constructor(options) { this.parent = options.element; this.cloneElementClassName = options.cloneElementClassName; this.isPointerdown = false; this.diff = { x: 0, y: 0 }; this.drag = { element: null, index: 0, lastIndex: 0 }; this.drop = { element: null, index: 0, lastIndex: 0 }; this.clone = { element: null, x: 0, y: 0 }; this.lastPointermove = { x: 0, y: 0 }; this.rectList = []; this.startPos = [0,0]; this.startTime = 0; this.init(); } init() { this.getRect(); this.bindEventListener(); } getRect() { this.rectList.length = 0; for (const item of this.parent.children) { this.rectList.push(item.getBoundingClientRect()); } } getDelta (pos1,pos2){ const [x1,y1]=pos1; const [x2,y2]=pos2; return Math.sqrt(Math.pow(x1-x2,2)+Math.pow(y1-y2,2)) } handlePointerdown(e) { if (e.pointerType === 'mouse' && e.button !== 0) { return; } if (e.target === this.parent) { return; } this.isPointerdown = true; this.parent.setPointerCapture(e.pointerId); this.lastPointermove.x = e.clientX; this.lastPointermove.y = e.clientY; this.startPos = [e.clientX, e.clientY]; this.startTime = (new Date).getTime(); this.drag.element = e.target; this.drag.element.classList.add('active'); this.clone.element = this.drag.element.cloneNode(true); this.clone.element.className = this.cloneElementClassName; this.clone.element.style.transition = 'none'; const i = [].indexOf.call(this.parent.children, this.drag.element); this.clone.x = this.rectList[i].left; this.clone.y = this.rectList[i].top; this.drag.index = i; this.drag.lastIndex = i; this.clone.element.style.transform = 'translate3d(' + this.clone.x + 'px, ' + this.clone.y + 'px, 0)'; document.body.appendChild(this.clone.element); } handlePointermove(e) { if (this.isPointerdown) { this.diff.x = e.clientX - this.lastPointermove.x; this.diff.y = e.clientY - this.lastPointermove.y; this.lastPointermove.x = e.clientX; this.lastPointermove.y = e.clientY; this.clone.x += this.diff.x; this.clone.y += this.diff.y; this.clone.element.style.transform = 'translate3d(' + this.clone.x + 'px, ' + this.clone.y + 'px, 0)'; for (let i = 0; i < this.rectList.length; i++) { if (e.clientX > this.rectList[i].left && e.clientX < this.rectList[i].right && e.clientY > this.rectList[i].top && e.clientY < this.rectList[i].bottom) { this.drop.element = this.parent.children[i]; this.drop.lastIndex = i; if (this.drag.element !== this.drop.element) { if (this.drag.index < i) { this.parent.insertBefore(this.drag.element, this.drop.element.nextElementSibling); this.drop.index = i - 1; } else { this.parent.insertBefore(this.drag.element, this.drop.element); this.drop.index = i + 1; } this.drag.index = i; const dragRect = this.rectList[this.drag.index]; const lastDragRect = this.rectList[this.drag.lastIndex]; const dropRect = this.rectList[this.drop.index]; const lastDropRect = this.rectList[this.drop.lastIndex]; this.drag.lastIndex = i; this.drag.element.style.transition = 'none'; this.drop.element.style.transition = 'none'; this.drag.element.style.transform = 'translate3d(' + (lastDragRect.left - dragRect.left) + 'px, ' + (lastDragRect.top - dragRect.top) + 'px, 0)'; this.drop.element.style.transform = 'translate3d(' + (lastDropRect.left - dropRect.left) + 'px, ' + (lastDropRect.top - dropRect.top) + 'px, 0)'; this.drag.element.offsetLeft; this.drag.element.style.transition = 'transform 150ms'; this.drop.element.style.transition = 'transform 150ms'; this.drag.element.style.transform = 'translate3d(0px, 0px, 0px)'; this.drop.element.style.transform = 'translate3d(0px, 0px, 0px)'; } break; } } } } handlePointerup(e) { if (this.isPointerdown) { this.isPointerdown = false; this.drag.element.classList.remove('active'); this.clone.element.remove(); let endPos = [e.clientX, e.clientY]; let endTime = (new Date).getTime(); logger.log('up',{ start:this.startPos, end:endPos, delta:this.getDelta(this.startPos,endPos), timediff:endTime - this.startTime, isclick: this.getDelta(this.startPos,endPos) < 10 && endTime - this.startTime < 800 }) if(this.getDelta(this.startPos,endPos) < 10 && endTime - this.startTime < 800){ this.drag.element.click(); } } } handlePointercancel(e) { if (this.isPointerdown) { this.isPointerdown = false; this.drag.element.classList.remove('active'); this.clone.element.remove(); } } bindEventListener() { this.handlePointerdown = this.handlePointerdown.bind(this); this.handlePointermove = this.handlePointermove.bind(this); this.handlePointerup = this.handlePointerup.bind(this); this.handlePointercancel = this.handlePointercancel.bind(this); this.getRect = this.getRect.bind(this); this.parent.addEventListener('pointerdown', this.handlePointerdown); this.parent.addEventListener('pointermove', this.handlePointermove); this.parent.addEventListener('pointerup', this.handlePointerup); this.parent.addEventListener('pointercancel', this.handlePointercancel); window.addEventListener('scroll', this.getRect); window.addEventListener('resize', this.getRect); window.addEventListener('orientationchange', this.getRect); } unbindEventListener() { this.parent.removeEventListener('pointerdown', this.handlePointerdown); this.parent.removeEventListener('pointermove', this.handlePointermove); this.parent.removeEventListener('pointerup', this.handlePointerup); this.parent.removeEventListener('pointercancel', this.handlePointercancel); window.removeEventListener('scroll', this.getRect); window.removeEventListener('resize', this.getRect); window.removeEventListener('orientationchange', this.getRect); } } MenuManager.setMenu("opensettings","打开设置",openSettingsModal); unsafeWindow.SNI_REFRESH = ()=>inject(); unsafeWindow.SNI_SETTINGS = openSettingsModal; startInject(); })().catch(e=>console.error("[SNI/ERR]",instance,e))