Greasy Fork

Greasy Fork is available in English.

[Bilibili] 视频旋转

旋转和缩放视频,防止某些视频伤害到你的脖子或眼睛!

当前为 2023-01-28 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         [Bilibili] 视频旋转
// @namespace    ckylin-script-bilibili-rotate
// @version      0.15
// @description  旋转和缩放视频,防止某些视频伤害到你的脖子或眼睛!
// @author       CKylinMC
// @match        https://www.bilibili.com/video/*
// @include      https://www.bilibili.com/medialist/play/*
// @include      https://www.bilibili.com/bangumi/play/*
// @include      https://bangumi.bilibili.com/anime/*/play*
// @include      https://bangumi.bilibili.com/movie/*
// @grant        GM_registerMenuCommand
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_getResourceText
// @grant        unsafeWindow
// @license      GPL-3.0-only
// ==/UserScript==
(function () {
    'use strict';
    let effects = [];
    /*
    * 实验性功能:
    * `injectToVideo`开关决定将面板注入到页面外围还是播放器中。
    *
    * 如果设置为 true 脚本会注入到播放器中,那么你可以在全屏时使用面板,但是由于b站
    * 切换视频会重置播放器,因此脚本将会持续观测播放器以响应变更
    * 此功能可能会导致页面卡顿或其他问题。
    *
    * 如果设置为 false 脚本会注入到页面外围,此时脚本注入完成后不会影响页面功能,
    * 但是如果你进入全屏模式将无法看到侧功能栏,脚本将只能通过快捷键来操作视频旋转缩放。
    *
    * 此选项由脚本自动维护,无需手动修改。
    */
    let injectToVideo = false;
    const loadInjectOption = (val=null) =>{
        if(val!==null){
            GM_setValue('ckrotate_injecttovideo',val);
        }
        let opt = GM_getValue('ckrotate_injecttovideo',false);
        if(opt===null||opt===undefined) opt = false;
        injectToVideo = opt;
    }
    loadInjectOption();
    const wait = (t) => {
        return new Promise(r => setTimeout(r, t));
    }
    const waitForPageVisible = async () => {
        return document.hidden && new Promise(r=>document.addEventListener("visibilitychange",r))
    }

    class EventEmitter {
        handlers = {};

        on(name, func) {
            if (!(func instanceof Function)) throw "Param must be func!";
            if (!(name in this.handlers)) {
                this.handlers[name] = [];
            }
            this.handlers[name].push(func);
        }

        off(name, func) {
            if (!(func instanceof Function)) throw "Param must be func!";
            if (name in this.handlers) {
                for (let i = 0; i < this.handlers[name].length; i++) {
                    if (this.handlers[name][i] === func) {
                        this.handlers[name].splice(i, 1);
                        i--;
                    }
                }
            }
        }

        emit(name, ...args) {
            if (name in this.handlers) {
                for (let func of this.handlers[name]) {
                    try {
                        func(...args);
                    } catch (e) {
                        console.error('ERROR:', e);
                    }
                }
            }
        }
    }

    class HoldClick {
        dom;
        emitter = new EventEmitter;
        downTime = 0;
        holdingTime = 250;
        mouseDown = false;

        constructor(dom, holdingTime = 250) {
            if (dom instanceof HTMLElement) {
                this.dom = dom;
                this.initListener();
            }
            this.holdingTime = holdingTime;
        }

        onclick(func) {
            this.emitter.on("click", func);
            return this;
        }

        onhold(func) {
            this.emitter.on("hold", func);
            return this;
        }

        onup(func) {
            this.emitter.on("up", func);
            return this;
        }

        offclick(func) {
            this.emitter.off("click", func);
            return this;
        }

        offhold(func) {
            this.emitter.off("hold", func);
            return this;
        }

        offup(func) {
            this.emitter.off("up", func);
            return this;
        }

        handleMouseDown(e) {
            e.preventDefault();
            this.mouseDown = true;
            this.downTime = (new Date()).getTime();
            setTimeout(() => {
                if (this.mouseDown) {
                    console.log(this);
                    this.mouseDown = false;
                    this.downTime = 0;
                    this.emitter.emit("hold", e);
                }
            }, this.holdingTime)
        }

        handleMouseUp(e) {
            e.preventDefault();
            if (this.mouseDown) {
                this.mouseDown = false;
                const currTime = (new Date).getTime();
                if ((currTime - this.downTime) >= this.holdingTime) {
                    this.emitter.emit("hold", e);
                } else {
                    this.emitter.emit("click", e);
                }
                this.downTime = 0;
            }
            this.emitter.emit("up", e);
        }

        handleMouseOut(e) {
            e.preventDefault();
            if (this.mouseDown) {
                this.mouseDown = false;
                this.downTime = 0;
                this.emitter.emit("hold", e);
            }
        }

        initListener() {
            this.dom.addEventListener("mouseup", this.handleMouseUp.bind(this))
            this.dom.addEventListener("mousedown", this.handleMouseDown.bind(this))
            this.dom.addEventListener("mouseout", this.handleMouseOut.bind(this))
        }
    }

    const dragger = {
        defaultHandler: (val) => console.log("DRAG:", val),
        waitForDragger: async (waitStatus = true) => {
            while (dragger.dragging !== waitStatus) await wait(10);
            return dragger;
        },
        regHandler: async (func) => {
            if (!(func instanceof Function)) throw "Param must be a func!";
            await dragger.waitForDragger(false);
            dragger.handler = func;
            return dragger;
        },
        handler: () => {
        },
        dragging: false,
        initialDragData: {
            x: 0,
            y: 0
        },
        lastDragData: {
            x: 0,
            y: 0
        },
        startDrag: (e) => {
            if (dragger.dragging) return;
            dragger.dragging = true;
            console.log(dragger.initialDragData);
            dragger.initialDragData.x = e.screenX;
            dragger.initialDragData.y = e.screenY;
            dragger.lastDragData.x = e.screenX;
            dragger.lastDragData.y = e.screenY;
            document.body.addEventListener("mouseup", dragger.stopDrag);
            document.body.addEventListener("mousemove", dragger.handleDrag);
            console.info("DRAG:", "Start Drag");
            return dragger;
        },
        handleDrag: (e) => {
            const currPos = {
                x: e.screenX,
                y: e.screenY
            };
            const initPos = dragger.initialDragData;
            const delta = {
                x: initPos.x - currPos.x,
                y: initPos.y - currPos.y
            }
            const lastdelta = {
                x: dragger.lastDragData.x - currPos.x,
                y: dragger.lastDragData.y - currPos.y
            }
            dragger.lastDragData = currPos;
            dragger.handler(delta, lastdelta);
        },
        stopDrag: () => {
            document.body.removeEventListener("mouseup", dragger.stopDrag);
            document.body.removeEventListener("mousemove", dragger.handleDrag);
            dragger.handler = dragger.defaultHandler;
            console.info("DRAG:", "Stop Drag");
            dragger.dragging = false;
            return dragger;
        },
    }

    async function playerReady() {
        let i = 50;
        while (--i >= 0) {
            await wait(100);
            if (!('player' in unsafeWindow)) continue;
            if (!('isInitialized' in unsafeWindow.player)) continue;
            if (!unsafeWindow.player.isInitialized()) continue;
        }
        if(i<0)return false;
        await waitForPageVisible();
        while(1){
            await wait(200);
            if(document.querySelector(".bilibili-player-video-control-wrap,.bpx-player-video-wrap")) return true;
        }
    }

    function bindKeys() {
        unsafeWindow.addEventListener("keypress", e => {
            if (e.key == "Q") {
                if (!e.shiftKey) return;
                if (["INPUT", "TEXTAREA"].includes(e.target.tagName)) return;
                leftR();
                e.preventDefault();
            } else if (e.key == "E") {
                if (!e.shiftKey) return;
                if (["INPUT", "TEXTAREA"].includes(e.target.tagName)) return;
                rightR();
                e.preventDefault();
            } else if (e.key == "A") {
                if (!e.shiftKey) return;
                if (["INPUT", "TEXTAREA"].includes(e.target.tagName)) return;
                smartLR();
                e.preventDefault();
            } else if (e.key == "D") {
                if (!e.shiftKey) return;
                if (["INPUT", "TEXTARREA"].includes(e.target.tagName)) return;
                smartRR();
                e.preventDefault();
            } else if (e.key == "R") {
                if (!e.shiftKey) return;
                if (["INPUT", "TEXTAREA"].includes(e.target.tagName)) return;
                cleanEffects();
                clearStyles();
                e.preventDefault();
            } else if (e.key == "+") {
                if (!e.shiftKey) return;
                if (["INPUT", "TEXTAREA"].includes(e.target.tagName)) return;
                zoomIn();
                e.preventDefault();
            } else if (e.key == "-") {
                if (!e.shiftKey) return;
                if (["INPUT", "TEXTAREA"].includes(e.target.tagName)) return;
                zoomOut();
                e.preventDefault();
            }
        });
    }

    function addEffects(name, value, wait = false) {
        let effect = effects.filter(e => e.name == name);
        if (effect.length) {
            effect[0].value += value;
        } else {
            effects.push({name, value});
        }
        if (!wait) applyEffects();
    }

    function setEffects(name, value, wait = false) {
        delEffect(name, true);
        addEffects(name, value, wait);
    }

    function delEffect(name, wait = false) {
        effects.forEach((e, i) => {
            if (e.name == name)
                effects.splice(i, 1);
        })
        if (!wait) applyEffects();
    }

    function applyEffects() {
        let style = ".bilibili-player-video video,.bilibili-player-video bwp-video,.bpx-player-video-wrap video { transform: ";
        effects.forEach(e => {
            let key = e.name;
            let value = e.value + "";
            switch (key) {
                case "rotate":
                    value += "deg";
                    break;
                case "translateY":
                case "translateX":
                    value += "px";
                    break;
                case "scale":
                    value = (1 - e.value) + "";
                    break;
            }
            style += ` ${key}(${value})`;
        });
        style += "}";
        console.log(style);
        clearStyles();
        addStyle(style);
    }

    function cleanEffects() {
        effects = [];
    }

    function clearStyles(className = "CKROTATE") {
        let dom = document.querySelectorAll("style." + className);
        if (dom) [...dom].forEach(e => e.remove());
    }

    function addStyle(s, className = "CKROTATE") {
        if (className !== "CKROTATE") clearStyles(className);
        let style = document.createElement("style");
        style.classList.add(className);
        style.innerHTML = s;
        document.body.appendChild(style);
    }

    function setR(val = 0) {
        setEffects("rotate", val);
    }

    function leftR(val = 90) {
        addEffects("rotate", val * -1);
    }

    function rightR(val = 90) {
        //debug
        //alert("rightR");
        addEffects("rotate", val);
    }

    function upR() {
        addEffects("rotate", 180);
    }

    function cR() {
        delEffect("rotate");
    }

    function setZ(val = 0) {
        setEffects("scale", val);
    }

    function zoomIn(val = 0.1) {
        addEffects("scale", val * -1);
    }

    function zoomOut(val = 0.1) {
        addEffects("scale", val);
    }

    function cZ() {
        delEffect("scale");
    }

    function setMY(val = 0) {
        setEffects("translateY", val);
    }

    function moveUp(val = 10) {
        addEffects("translateY", val * -1);
    }

    function moveDown(val = 10) {
        addEffects("translateY", val);
    }

    function setMX(val = 0) {
        setEffects("translateX", val);
    }

    function setPos(x, y) {
        setEffects("translateX", x, true);
        setEffects("translateY", y);
    }

    function movePos(x, y) {
        addEffects("translateX", x, true);
        addEffects("translateY", y);
    }

    function moveLeft(val = 10) {
        addEffects("translateX", val * -1);
    }

    function moveRight(val = 10) {
        addEffects("translateX", val);
    }

    function cM() {
        delEffect("translateX");
        delEffect("translateY");
    }

    function smartLR() {
        let dom = document.querySelector(".bilibili-player-video video,.bilibili-player-video bwp-video,.bpx-player-video-wrap video");
        if (!dom) return;
        let w = dom.videoWidth;
        let h = dom.videoHeight;
        let s = h / w;
        clearStyles();
        cleanEffects();
        addEffects("rotate", -90, true);
        addEffects("scale", 1 - s);
    }

    function smartRR() {
        let dom = document.querySelector(".bilibili-player-video video,.bilibili-player-video bwp-video,.bpx-player-video-wrap video");
        if (!dom) return;
        let w = dom.videoWidth;
        let h = dom.videoHeight;
        let s = h / w;
        clearStyles();
        cleanEffects();
        addEffects("rotate", 90, true);
        addEffects("scale", 1 - s);
    }

    function showTip() {
        addStyle(`
            #CKToast{
                background: white;
                position: fixed;
                top: 80px;
                right: 20px;
                border-radius: 3px;
                border-left: solid 4px #2196f3;
                padding: 10px;
                color: black;
                font-size: large;
                overflow: hidden;
                word-break: all;
                animation: CKToastIn cubic-bezier(0, 0, 0, 1.18) .5s forwards;
            }
            .dark #CKToast{
                background: #424242;
                color: white;
            }

            #CKToast button{
                border: none;
                background: #2196f3;
                color:white;
                padding:3px 6px;
                display: inline-block;
                margin: 3px;
                border-radius: 3px;
                cursor:pointer;
                font-size: medium;
                transition: all .3s;
            }
            #CKToast button:hover{
                filter: brightness(.5);
            }
            .dark #CKToast button{
                background: #1976d2;
            }
            @keyframes CKToastIn{
                from{
                    right: -100%;
                }
            }
            @keyframes CKToastOut{
                to{
                    right: -100%;
                }
            }
        `, "CKToastUIStyles");
        const toast = document.createElement("div");
        toast.id = "CKToast";
        toast.innerHTML = "检测到视频可能需要旋转<br>";
        const left = document.createElement("button");
        left.innerHTML = "左转90°";
        left.onclick = () => {
            smartLR();
            closeTip();
        }
        toast.appendChild(left);
        const right = document.createElement("button");
        right.innerHTML = "右转90°";
        right.onclick = () => {
            smartRR();
            closeTip();
        }
        toast.appendChild(right);
        const close = document.createElement("button");
        close.innerHTML = "关闭";
        close.style.background = "#d81b60";
        close.onclick = () => {
            closeTip();
        }
        toast.appendChild(close);
        document.body.appendChild(toast);
        setTimeout(closeTip, 10000);
    }

    function closeTip() {
        const toast = document.querySelector("#CKToast");
        if (toast) {
            toast.style.animation = null;
            toast.style.animation = "CKToastOut cubic-bezier(0.93, -0.32, 1, 1) .5s forwards";
            setTimeout(() => toast.remove(), 500);
        }
    }

    async function videoDetect() {
        if (!(await playerReady())) return;
        let dom = document.querySelector(".bilibili-player-video video,.bilibili-player-video bwp-video,.bpx-player-video-wrap video");
        if (!dom) return;
        let w = dom.videoWidth;
        let h = dom.videoHeight;
        if (h > w) {
            showTip();
        }
    }

    GM_registerMenuCommand("左转90", () => {
        leftR();
    });

    GM_registerMenuCommand("右转90°", () => {
        rightR();
    });

    GM_registerMenuCommand("智能左转90", () => {
        smartLR();
    });

    GM_registerMenuCommand("智能右转90°", () => {
        smartRR();
    });

    GM_registerMenuCommand("180°", () => {
        upR();
    });

    GM_registerMenuCommand("放大", () => {
        zoomIn();
    });

    GM_registerMenuCommand("缩小°", () => {
        zoomOut();
    });

    GM_registerMenuCommand("向上", () => {
        moveUp();
    });

    GM_registerMenuCommand("向下", () => {
        moveDown();
    });

    GM_registerMenuCommand("向左", () => {
        moveLeft();
    });

    GM_registerMenuCommand("向右", () => {
        moveRight();
    });

    GM_registerMenuCommand("清除旋转", () => {
        cR();
    });

    GM_registerMenuCommand("清除缩放", () => {
        cZ();
    });

    GM_registerMenuCommand("清除位移", () => {
        cM();
    });

    GM_registerMenuCommand("重置", () => {
        cleanEffects();
        clearStyles();
    });

    GM_registerMenuCommand("注入播放器", () => {
        if(confirm(`注入播放器功能(实验性)
    * 当前状态:${injectToVideo?"开":"关"}
    * 点击"确认"修改为 ${injectToVideo?"关":"开"} ,点击取消不做修改。
    * 
    * 说明
    * 
    * 此开关决定将面板注入到页面外围还是播放器中。
    *
    * 如果设置为 开 脚本会注入到播放器中,那么你可以在全屏时使用面板,但是由于b站
    * 切换视频会重置播放器,因此脚本将会持续观测播放器以响应变更。
    * 此功能可能会导致页面卡顿或其他问题(欢迎反馈遇到的问题)。
    *
    * 如果设置为 关 脚本会注入到页面外围,此时脚本注入完成后不会影响页面功能,
    * 但是如果你进入全屏模式将无法看到侧功能栏,脚本将只能通过快捷键来操作视频旋转缩放。`)){
            loadInjectOption(!injectToVideo);
            alert(`已修改为 ${injectToVideo?"开":"关"} 刷新页面生效`);
        }
    });

    /* Thanks for yoringboy's contributings! */
    function makeButton(icon, contents, color) {
        let ico = document.createElement("i");
        ico.classList.add("mdi", "mdi-18px", "mdi-" + icon);
        if (color) ico.style.color = color;
        let btn = document.createElement("div");
        btn.classList.add("ckrotate-btn");
        // btn.innerHTML = contents;
        btn.appendChild(ico);
        btn.setAttribute("data-btnname", contents);
        return btn
    }

    let lastObTimeout = null;
    let ignoreNextObEvent = false;
    let ob = null

    function injectButtons() {
        if(!document.querySelector("#mdiiconcss"))
            document.head.innerHTML += `<link id="mdiiconcss" rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/[email protected]/css/materialdesignicons.min.css"/>`
        addStyle(`
        #ckrotate-hidden-btn{
            z-index: 9999;
            position: fixed;
            left: -15px;
            width: 30px;
            height: 30px;
            background: black;
            opacity: 0.1;
            color: white;
            cursor: pointer;
            border-radius: 50%;
            text-align: right;
            line-height: 30px;
            transition: opacity .3s 1s, background .3s, color .3s;
            top: 120px;
            top: 30vh;
        }
        #ckrotate-hidden-btn:hover{
            transition: opacity .3s 0s, background .3s, color .3s;
            background: white;
            color: black;
            opacity: 0.75;
        }
        #ckrotate-hidden-btn.hide{
            left: -40px;
        }
        #ckrotate-btn-base{
            z-index: 9999;
            position: fixed;
            top: 55px;
            left: 20px;
            width: 110px;
            height: 450px;
            opacity: 0.75;
            background: black;
            color: white;
            text-align: center;
            cursor: pointer;
            flex: 1;
            border-radius: 8px;
            overflow: hidden;
            display: flex;
            flex-direction: column;
            flex-wrap: wrap;
            align-content: stretch;
            justify-content: space-between;
            align-items: center;
            max-height: 90vh;
            transition: all .3s;
        }
        #ckrotate-btn-base.hide{
            left: -120px;
            opacity: 0;
        }
        #ckrotate-btn-base .ckrotate-btn{
            display: flex;
            width: 55px;
            flex-flow: column;
            min-height: 55px;
            flex-wrap: nowrap;
            align-content: center;
            justify-content: center;
            align-items: center;
            transition: all .3s;
            border-radius: 8px;
            background: black;
        }
        #ckrotate-btn-base .ckrotate-btn:hover{
            background: white;
            color: transparent;
        }
        #ckrotate-btn-base .ckrotate-btn:hover>*{
            opacity: 0;
        }
        #ckrotate-btn-base .ckrotate-btn:hover::after{
            color: black;
            content: attr(data-btnname);
            transform: translateY(-80%);
        }
        `, "CKRotateBtnsStyle");
        const togglePanel = show => {
            const btn = document.querySelector("#ckrotate-hidden-btn");
            const panel = document.querySelector("#ckrotate-btn-base");
            if (show) {
                btn.className = "hide";
                panel.className = "show";
            } else {
                btn.className = "show";
                panel.className = "hide";
            }
        };
        const toggle = document.createElement("div");
        toggle.id = "ckrotate-hidden-btn";
        toggle.innerHTML = `<i class="mdi mdi-18px mdi-chevron-right"></i>`;
        toggle.onclick = () => togglePanel(true);
        const btnRoot = document.createElement("div");
        btnRoot.id = "ckrotate-btn-base";
        btnRoot.classList.add("hide");
        let toggleBtn = makeButton("chevron-left", "隐藏");
        toggleBtn.onclick = () => togglePanel(false);
        btnRoot.appendChild(toggleBtn);
        let LRBtn = makeButton("rotate-left", "左转90", "orange");
        new HoldClick(LRBtn)
            .onclick(() => {
                leftR();
            })
            .onhold(async (e) => {
                disableAnim();
                showArrows(btnRoot,false,{x:e.clientX,y:e.clientY},{down:"rotate-right",up:"rotate-left"});
                await (await dragger.regHandler((delta, lastdelta) => leftR(lastdelta.y))).startDrag(e).waitForDragger(false).then(() => {enableAnim();hideArrows()});
            })
        // LRBtn.onclick = function () {
        //     leftR();
        // };
        // LRBtn.oncontextmenu = async function (e){
        //     e.preventDefault();
        //     disableAnim();
        //     await (await dragger.regHandler(delta => {
        //         setR(delta.y*-1);
        //     })).startDrag(e);
        //     enableAnim();
        // };
        btnRoot.appendChild(LRBtn);
        let RRBtn = makeButton("rotate-right", "右转90", "orange");
        new HoldClick(RRBtn)
            .onclick(() => {
                rightR();
            })
            .onhold(async (e) => {
                disableAnim();
                showArrows(btnRoot,false,{x:e.clientX,y:e.clientY},{up:"rotate-right",down:"rotate-left"});
                await (await dragger.regHandler((delta, lastdelta) => rightR(lastdelta.y))).startDrag(e).waitForDragger(false).then(() => {enableAnim();hideArrows()});
            })
        // RRBtn.onclick = function () {
        //     rightR();
        // };
        // RRBtn.oncontextmenu = async function (e){
        //     e.preventDefault();
        //     disableAnim();
        //     await (await dragger.regHandler(delta => {
        //         setR(delta.y);
        //     })).startDrag(e);
        //     enableAnim();
        // };
        btnRoot.appendChild(RRBtn);
        let RVBtn = makeButton("rotate-3d-variant", "翻转", "orange");
        RVBtn.onclick = function () {
            upR();
        };
        btnRoot.appendChild(RVBtn);
        let SLRBtn = makeButton("undo", "智能左转", "yellow");
        SLRBtn.onclick = function () {
            smartLR();
        };
        btnRoot.appendChild(SLRBtn);
        let SRRBtn = makeButton("redo", "智能右转", "yellow");
        SRRBtn.onclick = function () {
            smartRR();
        };
        btnRoot.appendChild(SRRBtn);
        let ZIBtn = makeButton("arrow-expand-all", "放大", "cadetblue");
        new HoldClick(ZIBtn)
            .onclick(() => {
                zoomIn();
            })
            .onhold(async (e) => {
                disableAnim();
                showArrows(btnRoot,false,{x:e.clientX,y:e.clientY},{up:"arrow-expand-all",down:"arrow-collapse-all"});
                await (await dragger.regHandler((delta, lastdelta) => zoomIn(lastdelta.y * 0.01))).startDrag(e).waitForDragger(false).then(() => {enableAnim();hideArrows()});
            })
        // ZOBtn.onclick = function () {
        //     zoomIn();
        // };
        btnRoot.appendChild(ZIBtn);
        let ZOBtn = makeButton("arrow-collapse-all", "缩小", "cadetblue");
        new HoldClick(ZOBtn)
            .onclick(() => {
                zoomOut();
            })
            .onhold(async (e) => {
                disableAnim();
                showArrows(btnRoot,false,{x:e.clientX,y:e.clientY},{down:"arrow-expand-all",up:"arrow-collapse-all"});
                await (await dragger.regHandler((delta, lastdelta) => zoomOut(lastdelta.y * 0.01))).startDrag(e).waitForDragger(false).then(() => {enableAnim();hideArrows()});
            })
        // ZIBtn.onclick = function () {
        //     zoomOut();
        // };
        btnRoot.appendChild(ZOBtn);
        let MUBtn = makeButton("pan-up", "上移", "forestgreen");
        new HoldClick(MUBtn)
            .onclick(() => {
                moveUp();
            })
            .onhold(async (e) => {
                disableAnim();
                showArrows(btnRoot,true,{x:e.clientX,y:e.clientY});
                await (await dragger.regHandler((delta, lastdelta) => movePos(lastdelta.x*-1, lastdelta.y*-1))).startDrag(e).waitForDragger(false).then(() => {enableAnim();hideArrows()});
            })
        // MUBtn.onclick = function () {
        //     moveUp();
        // };
        btnRoot.appendChild(MUBtn);
        let MDBtn = makeButton("pan-down", "下移", "forestgreen");
        new HoldClick(MDBtn)
            .onclick(() => {
                moveDown();
            })
            .onhold(async (e) => {
                disableAnim();
                showArrows(btnRoot,true,{x:e.clientX,y:e.clientY});
                await (await dragger.regHandler((delta, lastdelta) => movePos(lastdelta.x*-1, lastdelta.y*-1))).startDrag(e).waitForDragger(false).then(() => {enableAnim();hideArrows()});
            })
        // MDBtn.onclick = function () {
        //     moveDown();
        // };
        btnRoot.appendChild(MDBtn);
        let MLBtn = makeButton("pan-left", "左移", "forestgreen");
        new HoldClick(MLBtn)
            .onclick(() => {
                moveLeft();
            })
            .onhold(async (e) => {
                disableAnim();
                showArrows(btnRoot,true,{x:e.clientX,y:e.clientY});
                await (await dragger.regHandler((delta, lastdelta) => movePos(lastdelta.x*-1, lastdelta.y*-1))).startDrag(e).waitForDragger(false).then(() => {enableAnim();hideArrows()});
            })
        // MLBtn.onclick = function () {
        //     moveLeft();
        // };
        btnRoot.appendChild(MLBtn);
        let MRBtn = makeButton("pan-right", "右移", "forestgreen");
        new HoldClick(MRBtn)
            .onclick(() => {
                moveRight();
            })
            .onhold(async (e) => {
                disableAnim();
                showArrows(btnRoot,true,{x:e.clientX,y:e.clientY});
                await (await dragger.regHandler((delta, lastdelta) => movePos(lastdelta.x*-1, lastdelta.y*-1))).startDrag(e).waitForDragger(false).then(() => {enableAnim();hideArrows()});
            })
        // MRBtn.onclick = function () {
        //     moveRight();
        // };
        btnRoot.appendChild(MRBtn);
        let CRBtn = makeButton("backup-restore", "清除旋转", "orange");
        CRBtn.onclick = function () {
            cR();
        };
        btnRoot.appendChild(CRBtn);
        let CZBtn = makeButton("magnify-remove-outline", "清除缩放", "cadetblue");
        CZBtn.onclick = function () {
            cZ();
        };
        btnRoot.appendChild(CZBtn);
        let CMBtn = makeButton("pan", "清除位移", "forestgreen");
        CMBtn.onclick = function () {
            cM();
        };
        btnRoot.appendChild(CMBtn);
        let RSBtn = makeButton("close-circle-outline", "重置", "orangered");
        RSBtn.onclick = function () {
            cleanEffects();
            clearStyles();
        };
        btnRoot.appendChild(RSBtn);
        let injectBase;
        ignoreNextObEvent = true;
        if(injectToVideo){
            injectBase = document.querySelector("#bilibiliPlayer") || document.body;
            if(ob===null){
                ob = new MutationObserver(()=>{
                    if(ignoreNextObEvent) return;
                    if(lastObTimeout) clearTimeout(lastObTimeout);
                    setTimeout(()=>{
                        injectButtons();
                        lastObTimeout = null;
                    },300);
                });
            }
            ob.observe(document.querySelector("#bilibiliPlayer"),{childList:true});
        }else{
            injectBase = document.body;
        }
        injectBase.appendChild(toggle);
        injectBase.appendChild(btnRoot);
        ignoreNextObEvent = false;
    }

    function showArrows(domRoot = document.body, horizontal=false, pos = {x:0,y:0},
                        iconlist = {}){
        let icons = {
            icon:iconlist.icon||"hand-right",
            up:iconlist.up||"arrow-up",
            down:iconlist.down||"arrow-down",
            left:iconlist.left||"arrow-left",
            right:iconlist.right||"arrow-right"
        }
        addStyle(`
        #CKROTATE-arrows-base{
            position:fixed;
            pointer-events: none;
            width: 60px;
            height: 60px;
            background: #00a1d6;
            box-shadow: 0px 0px 8px #00a1d6;
            transform: translate(-50%,-50%);
            text-align: center;
            border-radius: 50%;
            border: solid 3px #00a1d6;
        }
        .CKROTATE-arrow-item{
            padding:0;
            margin: 0 auto;
            line-height: 20px;
            /*height: 20px;*/
            color: white;
        }
        `,"CKROTATE-UI-ARROWS");
        hideArrows();
        const baseDom = document.createElement("div");
        baseDom.id="CKROTATE-arrows-base";
        baseDom.style.top = pos.y+"px";
        baseDom.style.left = pos.x+"px";

        const UpArrow = document.createElement("div");
        UpArrow.classList.add("CKROTATE-arrow-item");
        UpArrow.innerHTML = `<i class="mdi mdi-18px mdi-${icons.up}"></i>`;
        baseDom.appendChild(UpArrow);

        const LRArrow = document.createElement("div");
        LRArrow.classList.add("CKROTATE-arrow-item");
        if(horizontal){
            const LeftArrow = document.createElement("div");
            LeftArrow.classList.add("CKROTATE-arrow-item");
            LeftArrow.innerHTML = `<i class="mdi mdi-18px mdi-${icons.left}"></i>`;
            LeftArrow.style.float="left";
            LRArrow.appendChild(LeftArrow);
            const RightArrow = document.createElement("div");
            RightArrow.classList.add("CKROTATE-arrow-item");
            RightArrow.innerHTML = `<i class="mdi mdi-18px mdi-${icons.right}"></i>`;
            RightArrow.style.float="right";
            LRArrow.appendChild(RightArrow);
        }else{
            LRArrow.innerHTML = `<i class="mdi mdi-18px"></i>`;
        }
        baseDom.appendChild(LRArrow);

        const CenterPoint = document.createElement("div");
        CenterPoint.classList.add("CKROTATE-arrow-item");
        CenterPoint.innerHTML = `<i class="mdi mdi-18px mdi-${icons.icon}"></i>`;
        baseDom.appendChild(CenterPoint);

        const DownArrow = document.createElement("div");
        DownArrow.classList.add("CKROTATE-arrow-item");
        DownArrow.innerHTML = `<i class="mdi mdi-18px mdi-${icons.down}"></i>`;
        baseDom.appendChild(DownArrow);

        domRoot.appendChild(baseDom);
    }

    function hideArrows(){
        let olddom = document.querySelector("#CKROTATE-arrows-base");
        if(olddom) olddom.remove();
    }

    function enableAnim() {
        addStyle(".bilibili-player-video video,.bilibili-player-video bwp-video,.bpx-player-video-wrap video{transition: transform cubic-bezier(0.61, 0.01, 0.44, 0.93) .5s;}", "CKANIMATION");
    }

    function disableAnim() {
        clearStyles("CKANIMATION");
    }

    async function startInject() {
        enableAnim();
        bindKeys();
        await videoDetect();
        injectButtons();
    }

    startInject()
        .then(()=>console.log("[CKROTATE]","Inject task completed."))
        .catch(e=>console.error("[CKROTATE]","Inject task failed:",e));
})();