// ==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 result;
if(mod.constructor.name=='AsyncFunction'){
result = await mod(...args);
}else {
result = mod(...args);
}
if(result){
if(result instanceof HTMLElement||result instanceof Node){
wrapper.appendChild(result);
logger.log(name,"dom",result);
}else if(result instanceof String || typeof result=='string' || typeof result=='number'){
wrapper.appendChild(document.createTextNode(result));
logger.log(name,"text",result);
}else{
logger.log(name,"unknownresult","skipped",result);
}
}else{
logger.log(name,"noresult","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))