Greasy Fork is available in English.

bilibili H5播放器快捷操作

快捷键设置,回车快速发弹幕,双击全屏,自动选择最高清画质、播放、全屏、关闭弹幕、自动转跳和自动关灯等

// ==UserScript==
// @name         bilibili H5播放器快捷操作
// @namespace    https://github.com/jeayu/bilibili-quickdo
// @version      0.9.9.9
// @description  快捷键设置,回车快速发弹幕,双击全屏,自动选择最高清画质、播放、全屏、关闭弹幕、自动转跳和自动关灯等
// @author       jeayu
// @license      MIT
// @match        *://www.bilibili.com/video/av*
// @match        *://www.bilibili.com/video/bv*
// @match        *://www.bilibili.com/video/BV*
// @grant        GM_setValue
// @grant        GM_getValue
// ==/UserScript==

(function () {
    'use strict';
    const q = function (selector) {
        let nodes = [];
        if (typeof selector === 'string') {
            Object.assign(nodes, document.querySelectorAll(selector));
            nodes.selectorStr = selector;
        } else if (selector instanceof NodeList) {
            Object.assign(nodes, selector);
        } else if (selector instanceof Node) {
            nodes = [selector];
        }
        nodes.click = function (index = 0) {
            nodes.length > index && nodes[index].click();
            return this;
        }
        nodes.addClass = function (classes, index = 0) {
            nodes.length > index && nodes[index].classList.add(classes);
            return this;
        }
        nodes.removeClass = function (classes, index = 0) {
            nodes.length > index && nodes[index].classList.remove(classes);
            return this;
        }
        nodes.text = function (index = 0) {
            return nodes.length > index && nodes[index].textContent || '';
        }
        nodes.css = function (name, value, index = 0) {
            nodes.length > index && nodes[index].style.setProperty(name, value);
            return this;
        }
        nodes.getCss = function (name, index = 0) {
            return nodes.length > index && nodes[index].ownerDocument.defaultView.getComputedStyle(nodes[index], null).getPropertyValue(name);
        }
        nodes.mouseover = function (index = 0) {
            return this.trigger('mouseover', index);
        }
        nodes.mouseout = function (index = 0) {
            return this.trigger('mouseout', index);
        }
        nodes.attr = function (name, index = 0) {
            const result = nodes.length > index ? nodes[index].attributes[name] : undefined;
            return result && result.value;
        }
        nodes.hasClass = function (className, index = 0) {
            return nodes.length > index && nodes[index].className.match && (nodes[index].className.match(new RegExp(`(\\s|^)${className}(\\s|$)`)) != null);
        }
        nodes.append = function (text, where = 'beforeend', index = 0) {
            nodes.length > index && nodes[index].insertAdjacentHTML(where, text);
            return this;
        }
        nodes.find = function (name, index = 0) {
            return q(nodes[index].querySelectorAll(name));
        }
        nodes.toggleClass = function (className, flag, index = 0) {
            return flag ? this.addClass(className, index) : this.removeClass(className, index);
        }
        nodes.next = function (index = 0) {
            return nodes.length > index && nodes[index].nextElementSibling ? q(nodes[index].nextElementSibling) : [];
        }
        nodes.prev = function (index = 0) {
            return nodes.length > index && nodes[index].previousElementSibling ? q(nodes[index].previousElementSibling) : [];
        }
        nodes.trigger = function (event, index = 0) {
            if (nodes.length > index) {
                const evt = document.createEvent('Event');
                evt.initEvent(event, true, true);
                nodes[index].dispatchEvent(evt);
            }
            return this;
        }
        nodes.last = function () {
            return q(nodes[nodes.length - 1]);
        }
        nodes.on = function (event, fn, useCapture=false, index = 0) {
            nodes.length > index && nodes[index].addEventListener(event, fn, useCapture);
            return this;
        }
        nodes.select = function (index = 0) {
            nodes.length > index && nodes[index].select();
            return this;
        }
        nodes.blur = function (index = 0) {
            nodes.length > index && nodes[index].blur();
            return this;
        }
        nodes.val = function (value, index = 0) {
            if (value) {
                nodes[index].value = value;
                return this;
            }
            return nodes[index].value;
        }
        nodes.offset = function (index = 0) {
            if (nodes.length <= index) {
                return {top: 0, left: 0};
            }
            const rect = nodes[index].getBoundingClientRect();
            return {top: rect.top + document.body.scrollTop, left: rect.left + document.body.scrollLeft}
        }
        nodes.parseFloat = function (css, index = 0) {
            return (parseFloat(this.getCss(css, index)) || 0);
        }
        nodes.after = function (node, index = 0) {
            nodes.length > index && node instanceof Node && nodes[index].parentNode.insertBefore(node, nodes[index]);
            return this;
        }
        return nodes;
    }
    const SELECTOR = {
        h5Player: "#bilibili-player video",
        playerContainer: ".bpx-player-container",
        playerControl: ".bpx-player-control-wrap",
        senderBarArea: ".bpx-player-sending-area",
        playerFull: ".bpx-player-ctrl-full",
        playerWeb: ".bpx-player-ctrl-web",
        playerWide: ".bpx-player-ctrl-wide",
        dmInput: "input.bpx-player-dm-input",
        dmInputFocus: "input.bpx-player-dm-input:focus",
        playerCurrentTime: ".bpx-player-ctrl-time-current",
        mirror: ".bpx-player-ctrl-setting-mirror input",
        lightOff: ".bpx-player-ctrl-setting-lightoff input",
        jumpContent: ".bpx-player-electric-jump",
        dmSwitch: ".bpx-player-dm-switch input",
        dmTypeScroll: ".bpx-player-block-typeScroll",
        dmTypeTop: ".bpx-player-block-typeTop",
        dmTypeBottom: ".bpx-player-block-typeBottom",
        dmTypeColor: ".bpx-player-block-typeColor",
        dmTypeSpecial: ".bpx-player-block-typeSpecial",
        videoQuality: ".bpx-player-ctrl-quality-menu-item",
        vipVideoQuality: ".bpx-player-ctrl-quality-badge-bigvip",
        moreDescribe: "#v_desc .toggle-btn",
        playerVideoArea: ".bpx-player-video-area",
        playerCtrlPrev: ".bpx-player-ctrl-prev",
        playerCtrlNext: ".bpx-player-ctrl-next",
    };
    const [ON, OFF] = [1, 0];
    const [FULLSCREEN, WEBFULLSCREEN, WIDESCREEN, DEFAULT, MINI] = ["full", "web", "wide", "normal", "mini"];
    const KEY_BOARD = {
        keyCode: {
            'enter': 13,
            'esc': 27,
            '=': 187,
            '-': 189,
            '0': 48,
            '1': 49,
            '2': 50,
            '3': 51,
            '4': 52,
            '5': 53,
            '6': 54,
            '7': 55,
            '8': 56,
            '9': 57,
            'a': 65,
            'b': 66,
            'c': 67,
            'd': 68,
            'e': 69,
            'f': 70,
            'g': 71,
            'h': 72,
            'i': 73,
            'j': 74,
            'k': 75,
            'l': 76,
            'm': 77,
            'n': 78,
            'o': 79,
            'p': 80,
            'q': 81,
            'r': 82,
            's': 83,
            't': 84,
            'u': 85,
            'v': 86,
            'w': 87,
            'x': 88,
            'y': 89,
            'z': 90,
            'left': 37,
            'up': 38,
            'right': 39,
            'down': 40,
            'space': 32,
            '[': 219,
            ']': 221,
            '\\': 220,
            ';': 186,
            "'": 222,
            ',': 188,
            '.': 190,
            '/': 191,
            '`': 192,
        },
        defaultShortCut: {
            f: { text: 'f键', status: ON, bindKey: 'f' },
            leftSquareBracket: { text: '[键', status: ON, bindKey: '[' },
            rightSquareBracket: { text: ']键', status: ON, bindKey: ']' },
            enter: { text: '回车键', status: OFF, bindKey: 'enter' },
            up: { text: '↑键', status: OFF, bindKey: 'up' },
            down: { text: '↓键', status: OFF, bindKey: 'down' },
            right: { text: '←键', status: OFF, bindKey: 'left' },
            left: { text: '→键', status: OFF, bindKey: 'right' },
            space: { text: '空格键播放/暂停', status: OFF, bindKey: 'space' },
            q: { text: 'q键-点赞', status: ON, bindKey: 'q' },
            w: { text: 'w键-投币', status: ON, bindKey: 'w' },
            e: { text: 'e键-收藏', status: ON, bindKey: 'e' },
            r: { text: 'r键-长按三连', status: ON, bindKey: 'r' },
            d: { text: 'd键-关闭弹幕', status: ON, bindKey: 'd' },
            m: { text: 'm键-静音', status: ON, bindKey: 'm' },
        },
        getKeyCode(key) {
            return this.keyCode[key];
        },
        isGlobalHotKey(keyCode) {
            return ["left", "up", "right", "down", "space"].find(key => this.getKeyCode(key) == keyCode);
        },
        isNumberKeyCode(keyCode) {
            return keyCode >= this.getKeyCode('0') && keyCode <= this.getKeyCode('9');
        },
        getDefaultShortCutStatus(key) {
            return STORAGE.getDefaultShortCutStatus(key, this.defaultShortCut[key].status);
        },
        checkDefaultShortCut(keyCode) {
            return Object.entries(this.defaultShortCut)
                .some(([key, { text, status, bindKey }]) => this.getKeyCode(bindKey) == keyCode && this.getDefaultShortCutStatus(key)== ON);
        },
    };
    const H5_PLAYER = {
        variable: {
            speed: { text: '播放速度调整倍数', value: 0.25 },
            minSpeed: { text: '最小播放速度倍数', value: 0.25 },
            maxSpeed: { text: '最大播放速度倍数', value: 4 },
            volume: { text: '音量调整百分比', value: 1 },
            videoProgress: { text: '快进/快退调整秒数', value: 1 },
        },
        getVarSetting(key) {
            return this.variable[key].value;
        },
        init() {
            this.h5Player = q(SELECTOR.h5Player);
            this.control = q(SELECTOR.playerControl);
            this.senderBar = q(SELECTOR.senderBarArea);
            this.played = false;
            console.log('H5_PLAYER done');
        },
        addEventListener(event, func) {
            this.h5Player.on(event, func);
        },
        addPlayingEvent(func) {
            this.addEventListener('playing', func);
        },
        addPauseEvent(func) {
            this.addEventListener('pause', func);
        },
        addEndedEvent(func) {
            this.addEventListener('ended', func);
        },
        getDuration() {
            return this.h5Player[0].duration;
        },
        getCurrentTime() {
            return this.h5Player[0].currentTime;
        },
        setVideoCurrentTime(time) {
            if (time > -1 && time <= this.h5Player[0].duration) {
                this.h5Player[0].currentTime = time;
            }
        },
        play() {
            this.h5Player[0].play();
        },
        pause() {
            this.h5Player[0].pause();
        },
        isPaused(){
            return this.h5Player[0].paused;
        },
        playAndPause() {
            this.isPaused() ? this.play() : this.pause();
        },
        subSpeed() {
            this.h5Player[0].playbackRate = Math.max(this.h5Player[0].playbackRate - this.getVarSetting('speed'), this.getVarSetting('minSpeed'));
        },
        addSpeed() {
            this.h5Player[0].playbackRate = Math.min(this.h5Player[0].playbackRate + this.getVarSetting('speed'), this.getVarSetting('maxSpeed'));
        },
        resetSpeed() {
            this.h5Player[0].playbackRate = 1;
        },
        getPlaybackRate() {
            return this.h5Player[0].playbackRate;
        },
        getVolume() {
            return this.h5Player[0].volume;
        },
        setVolume(volume) {
            this.h5Player[0].volume = volume;
        },
        mute() {
            this.h5Player[0].muted = !this.h5Player[0].muted;
        },
        subVolume() {
            this.setVolume(Math.max(this.getVolume() - (this.getVarSetting('volume') / 100), 0));
        },
        addVolume() {
            this.setVolume(Math.min(this.getVolume() + (this.getVarSetting('volume') / 100), 1));
        },
        subProgress() {
            this.h5Player[0].currentTime -= this.getVarSetting('videoProgress');
        },
        addProgress() {
            this.h5Player[0].currentTime += this.getVarSetting('videoProgress');
        },
        offsetTop() {
            return this.h5Player.offset().top;
        }
    };
    const UI = {
        addStyle(css, id) {
            id = id ? `id=${id}` : '';
            q('head').append(`<style ${id} type="text/css">${css}</style>`);
        },
        removeStyle(styleId) {
            const styleNode = q(styleId)[0];
            styleNode && styleNode.parentNode.removeChild(styleNode);
        },
        showHint(info, hideVolumeIcon=true) {
            clearTimeout(this.hintTimer);
            q('div.bpx-player-volume-hint').length || this.initShowHint();
            q('div.bpx-player-volume-hint').css('opacity', 1).css('display', '');

            q('span.bpx-player-volume-hint-icon').css('display', hideVolumeIcon ? 'none' : '');
            q('span.bpx-player-volume-hint-text')[0].innerHTML = info;
            this.hintTimer = setTimeout(() => {
                q('div.bpx-player-volume-hint').css('opacity', 0).css('display', 'none');
                q('span.bpx-player-volume-hint-icon').css('display', '');
            }, 1E3);
        },
        initShowHint() {
            const html = `
            <div class="bpx-player-volume-hint" style="opacity: 0; display: none;">
                <span class="bpx-player-volume-hint-icon"><span class="bpx-common-svg-icon">
                    <svg viewBox="0 0 22 22">
                        <use xlink:href="#bpx-svg-sprite-volume"></use>
                    </svg>
                </span>
            <span class="bpx-common-svg-icon" style="display: none;">
                <svg viewBox="0 0 22 22">
                    <use xlink:href="#bpx-svg-sprite-volume-off"></use>
                </svg>
            </span>
            </span>
                <span class="bpx-player-volume-hint-text">70%</span>
            </div>
            `;
            q(SELECTOR.playerVideoArea).append(html);
        },
        hideSenderBar() {
            this.hideSenderBarCss();
        },
        hideSenderBarCss() {
            const css = `${SELECTOR.senderBarArea}{opacity: 0!important;display: none!important}`;
            this.addStyle(css, 'qd-hideSenderBar');
        },
        showSenderBar() {
            H5_PLAYER.senderBar.css('opacity', 1).css('display', '');
            this.removeStyle('#qd-hideSenderBar');
        },
        h5PlayerRotate(flag, rotationDeg=90) {
            const h5Player = H5_PLAYER.h5Player[0];
            const deg = this.getRotationDeg(H5_PLAYER.h5Player) + rotationDeg * flag;
            let transform = `rotate(${deg}deg)`;
            if (deg == 0 || deg == 180 * flag) {
                transform += ` scale(1)`;
            } else {
                transform += ` scale(${h5Player.videoHeight / h5Player.videoWidth})`;
            }
            this.setH5PlayerRransform(transform);
        },
        setH5PlayerRransform(transform) {
            H5_PLAYER.h5Player.css('-webkit-transform', transform)
                .css('-moz-transform', transform)
                .css('-ms-transform', transform)
                .css('-o-transform', transform)
                .css('transform', transform);
        },
        getTransformCss(e) {
            return e.getCss('-webkit-transform') || e.getCss('-moz-transform') || e.getCss('-ms-transform') || e.getCss('-o-transform') || 'none';
        },
        getRotationDeg(e) {
            const transformCss = this.getTransformCss(e);
            let matrix = transformCss.match('matrix\\((.*)\\)');
            if (matrix) {
                matrix = matrix[1].split(',');
                if (matrix) {
                    const rad = Math.atan2(matrix[1], matrix[0]);
                    return (rad * 180 / Math.PI) | 0;
                }
            }
            return 0;
        },
        isWide() {
            return q(SELECTOR.playerContainer).attr('data-screen') == WIDESCREEN || q(SELECTOR.playerWide).hasClass('bpx-state-entered');
        },
        ultraWidescreenCss() {
            this.removeStyle('#qd-ultraWidescreen');
            const clientWidth = document.body.clientWidth;
            const marginLeft = q('#bilibili-player ').offset().left;
            const css = `
            .bpx-player-container[data-screen=wide] {
                width:${clientWidth}px!important;margin-left:-${marginLeft}px!important;z-index: 1000!important;
            }
            `;
            this.addStyle(css, 'qd-ultraWidescreen');
        },
        rConCss() {
            this.removeStyle('#qd-rCon');
            if (!this.isBottomTitle || !this.isWide()) {
                return;
            }
            const top = H5_PLAYER.h5Player.parseFloat('height');
            const css = `
            .r-con{margin-top:${top}px!important}
            .right-container{margin-top:${top}px!important}
            `;
            this.addStyle(css, 'qd-rCon');
        },
        bottomTitle() {
            q('#viewbox_report').after(q('#playerWrap')[0]);
            if (!this.isBottomTitle) {
                const css = `#viewbox_report {height:auto!important;}`;
                this.addStyle(css, 'qd-bottomTitle');
                this.isBottomTitle = true;
            }
        },
        removeFixedHeader() {
            q('.bili-header').removeClass('fixed-header');
        },
    };
    const REPEAT_CONTROLLER = {
        repeatStart: undefined,
        repeatEnd: undefined,
        setRepeatStart() {
            this.repeatStart = H5_PLAYER.getCurrentTime();
            UI.showHint(`起点 ${q(SELECTOR.playerCurrentTime).text()}`);
        },
        setRepeatEnd() {
            this.repeatEnd = H5_PLAYER.getCurrentTime();
            UI.showHint(`终点 ${q(SELECTOR.playerCurrentTime).text()}`);
        },
        resetRepeat() {
            this.repeatEnd = this.repeatStart = undefined;
            UI.showHint(`清除循环点`)
        },
        check() {
            return this.repeatEnd && this.repeatStart && this.repeatEnd <= H5_PLAYER.getCurrentTime();
        },
        start() {
            H5_PLAYER.setVideoCurrentTime(this.repeatStart);
        }
    };
    const CONTROLLER = {
        keydownFn: undefined,
        hintTimer: undefined,
        config: {
            globalHotKey: { text: '快捷键设置全局', status: OFF, tips: '上下左右空格不会滚动页面' },
        },
        quickDo: {
            settingPanel: { value: '`', text: '设置面板', },
            fullscreen: { value: 'f', text: '全屏', },
            webFullscreen: { value: 'w', text: '网页全屏', },
            widescreen: { value: 'q', text: '宽屏', },
            subSpeed: { value: '[', text: '减少速度', },
            addSpeed: { value: ']', text: '增加速度', },
            resetSpeed:  { value: '\\', text: '重置速度', },
            danmu: { value: 'd', text: '弹幕', },
            playAndPause: { value: 'p', text: '暂停播放', },
            prevPart: { value: 'k', text: '上一P', },
            nextPart: { value: 'l', text: '下一P', },
            showDanmuInput: { value: 'enter', text: '发弹幕', },
            mirror: { value: 'j', text: '镜像', },
            danmuTop: { value: 't', text: '顶部弹幕', },
            danmuBottom: { value: 'b', text: '底部弹幕', },
            danmuScroll: { value: 's', text: '滚动弹幕', },
            danmuSpecial: { value: '', text: '高级弹幕', },
            danmuColor: { value: 'c', text: '彩色弹幕', },
            rotateRight: { value: 'o', text: '向右旋转', },
            rotateLeft: { value: 'i', text: '向左旋转', },
            lightOff: { value: 'y', text: '灯', },
            mute: { value: 'm', text: '静音', },
            scroll2Top: { value: ';', text: '回到顶部', },
            jumpContent: { value: 'g', text: '跳过鸣谢', },
            playerSetOnTop: { value: "'", text: '播放器置顶', },
            setRepeatStart: { value: ',', text: '循环起点', },
            setRepeatEnd: { value: '.', text: '循环终点', },
            resetRepeat: { value: '/', text: '清除循环点', },
            subVolume: { value: '', text: '减少音量', },
            addVolume: { value: '', text: '增加音量', },
            subProgress: { value: '', text: '快退', },
            addProgress: { value: '', text: '快进', },
        },
        initKeyDown() {
            this.keydownFn = this.keydownFn || (e=> !q('input:focus, textarea:focus').length && this.keyHandler(e));
            q(document).on('keydown', this.keydownFn, true);
            console.log('initKeyDown done');
        },
        keyHandler(e) {
            const {keyCode, ctrlKey, shiftKey, altKey} = e;
            if (ctrlKey || shiftKey || altKey) {
                return;
            }
            if (KEY_BOARD.checkDefaultShortCut(keyCode)) {
                e.stopPropagation();
            }
            if (this.getControllerConfigStatus('globalHotKey') === ON) {
                if (KEY_BOARD.isGlobalHotKey(keyCode)) {
                    this.focusPlayer();
                    e.preventDefault();
                }
            }

            Object.keys(this.quickDo)
                .some(quickDoKey => keyCode === this.getQuickDoKeyCode(quickDoKey) && (!this[quickDoKey]() || !e.preventDefault())) ||
                this.numberKeySkip(keyCode);
            e.defaultPrevented;
        },
        bindDanmuInputKeydown() {
            q(SELECTOR.dmInput).on('keydown', e => {
                e.keyCode === KEY_BOARD.keyCode.enter && this.hideDanmuInput();
            });
        },
        getControllerConfigStatus(key) {
            return STORAGE.getControllerConfigStatus(key, this.config[key].status);
        },
        getQuickDoKeyCode(quickDoKey) {
            return KEY_BOARD.getKeyCode(STORAGE.getQuickDoKey(quickDoKey, this.quickDo[quickDoKey].value));
        },
        focusPlayer() {
            H5_PLAYER.control.click();
        },
        numberKeySkip(keyCode) {
            if (KEY_BOARD.isNumberKeyCode(keyCode)) {
                const multiple = keyCode - KEY_BOARD.getKeyCode('0');
                H5_PLAYER.setVideoCurrentTime(H5_PLAYER.getDuration()/ 10 * multiple);
            }
        },
        triggerSleep(el, event='click', ms=100) {
            return new Promise((resolve, reject) => {
                if (el && el[0]) {
                    el.trigger(event);
                    setTimeout(resolve, ms);
                } else {
                    reject();
                }
            });
        },
        mode(mode, check=true) {
            const curMode = q(SELECTOR.playerContainer).attr('data-screen');
            if (check && mode == curMode) {
                return;
            }
            switch (mode) {
                case FULLSCREEN: return this.fullscreen();
                case WEBFULLSCREEN: return this.webFullscreen();
                case WIDESCREEN: return this.widescreen();
                case MINI: return q(SELECTOR.playerContainer)[0].setAttribute('data-screen', 'mini');
                case DEFAULT: return curMode == MINI ? q(SELECTOR.playerContainer)[0].setAttribute('data-screen', DEFAULT) : this.mode(curMode, false);
                default: return;
            }
        },
        ultraWidescreen() {
            this.mode(WIDESCREEN);
            UI.ultraWidescreenCss();
        },
        // ---------------
        settingPanel() {
            SETTING_PANEL.switch();
        },
        fullscreen() {
            q(SELECTOR.playerFull).click();
        },
        webFullscreen() {
            q(SELECTOR.playerWeb).click();
        },
        widescreen() {
            q(SELECTOR.playerWide).click();
        },
        subSpeed() {
            H5_PLAYER.subSpeed();
            UI.showHint("速度: " + H5_PLAYER.getPlaybackRate() + "x");
        },
        addSpeed() {
            H5_PLAYER.addSpeed();
            UI.showHint("速度: " + H5_PLAYER.getPlaybackRate() + "x");
        },
        resetSpeed() {
            H5_PLAYER.resetSpeed();
            UI.showHint("速度: " + H5_PLAYER.getPlaybackRate() + "x");
        },
        danmu() {
            q(SELECTOR.dmSwitch).click();
        },
        playAndPause() {
            H5_PLAYER.playAndPause();
        },
        prevPart() {
            q(SELECTOR.playerCtrlPrev).click()
        },
        nextPart() {
            q(SELECTOR.playerCtrlNext).click();
        },
        showDanmuInput() {
            UI.showSenderBar();
            const danmuInput = q(SELECTOR.dmInput);
            if (!q(SELECTOR.dmInputFocus).length) {
                this.triggerSleep(danmuInput, 'mouseover').then(() => {
                    danmuInput.select().click();
                }).catch(() => {});
            }
        },
        hideDanmuInput() {
            UI.hideSenderBar();
            const danmuInput = q(SELECTOR.dmInput);
            this.triggerSleep(danmuInput, 'mouseout').then(() => {
                danmuInput.blur();
                this.focusPlayer();
            }).catch(() => {});
        },
        mirror() {
            UI.setH5PlayerRransform('');
            q(SELECTOR.mirror).click();
        },
        danmuType(selector) {
            q(selector).click();
            const text = q(`${selector} .bpx-player-block-filter-label`).text();
            if (q(`${selector}.bpx-player-active`).length) {
                UI.showHint(`开启${text}`);
            } else {
                UI.showHint(`关闭${text}`);
            }
        },
        danmuTop() {
            this.danmuType(SELECTOR.dmTypeTop);
        },
        danmuBottom() {
            this.danmuType(SELECTOR.dmTypeBottom);
        },
        danmuScroll() {
            this.danmuType(SELECTOR.dmTypeScroll);
        },
        danmuColor() {
            this.danmuType(SELECTOR.dmTypeColor);
        },
        danmuSpecial() {
            this.danmuType(SELECTOR.dmTypeSpecial);
        },
        rotateRight() {
            UI.h5PlayerRotate(1);
        },
        rotateLeft() {
            UI.h5PlayerRotate(-1);
        },
        isLightOff() {
            return q(SELECTOR.lightOff)[0].checked;
        },
        lightOff() {
            q(SELECTOR.lightOff).click();
        },
        light(status) {
            if (status == ON && this.isLightOff()) {
                q(SELECTOR.lightOff).click();
            } else if (status == OFF && !this.isLightOff()) {
                q(SELECTOR.lightOff).click();
            }
        },
        mute() {
            H5_PLAYER.mute();
        },
        scroll2Top() {
            window.scrollTo(0, 0);
        },
        jumpContent() {
            q(SELECTOR.jumpContent).click();
        },
        playerSetOnTop() {
            this.scroll2Top();
            window.scrollTo(0, H5_PLAYER.offsetTop());
        },
        setRepeatStart() {
            REPEAT_CONTROLLER.setRepeatStart();
        },
        setRepeatEnd() {
            REPEAT_CONTROLLER.setRepeatEnd();
        },
        resetRepeat() {
            REPEAT_CONTROLLER.resetRepeat();
        },
        subVolume() {
            H5_PLAYER.subVolume();
            UI.showHint(`${Math.ceil(H5_PLAYER.getVolume() * 100)}%`, false);
        },
        addVolume() {
            H5_PLAYER.addVolume();
            UI.showHint(`${Math.ceil(H5_PLAYER.getVolume() * 100)}%`, false);
        },
        subProgress() {
            H5_PLAYER.subProgress();
        },
        addProgress() {
            H5_PLAYER.addProgress();
        },
        // ---------------
    };
    const AUTOMATON = {
        playerCheckbox: {
            options: {
                hideSenderBar: { text: '隐藏弹幕栏', status: ON, fn: 'hideOrShowSenderBar', tips: '发弹幕快捷键可显示' },
                widescreenScroll2Top: { text: '宽屏时回到顶部', status: OFF, ban:['widescreenPlayerSetOnTop'], fn: 'setWidescreenPos' },
                widescreenPlayerSetOnTop: { text: '宽屏时播放器置顶部', status: ON, ban:['widescreenScroll2Top'], fn: 'setWidescreenPos' },
                lightOffWhenPlaying: { text: '播放时自动关灯', status: OFF, },
                lightOnWhenPause: { text: '暂停时自动开灯', status: OFF, },
                screenWhenPause: { text: '暂停还原屏幕', status: OFF, },
                ultraWidescreen: { text: '超宽屏', status: ON, fn: 'ultraWidescreen', tips: '宽屏模式宽度和窗口一样'},
                bottomTitle: { text: '标题位于播放器下方', status: ON, tips: '刷新生效' },
            },
            btn: '播放器设置',
        },
        startCheckbox: {
            options: {
                lightOff: { text: '自动关灯', status: OFF },
                webFullscreen: { text: '自动网页全屏', status: OFF, ban:['widescreen'] },
                widescreen: { text: '自动宽屏', status: ON, ban:['webFullscreen'] },
                highQuality: { text: '自动最高画质', status: ON, ban:['vipHighQuality'] },
                vipHighQuality: { text: '自动最高画质(大会员使用)', status: OFF, ban:['highQuality'] },
                vipHighQualityNot4K: { text: '自动最高画质不选择4K', status: OFF },
                moreDescribe: { text: '自动展开视频简介', status: ON },
            },
            btn: '播放前自动设置',
        },
        endCheckbox: {
            options: {
                lightOn: { text: '播放结束自动开灯', status: ON, tips: '还有下一P不触发' },
                exitScreen: { text: '播放结束还原屏幕', status: ON, ban:['exit2WideScreen'], tips: '还有下一P不触发' },
                exit2WideScreen: { text: '播放结束还原宽屏', status: OFF, ban:['exitScreen'], tips: '还有下一P不触发' },
                autoJumpContent: { text: '跳过充电鸣谢', status: ON },
            },
            btn: '播放结束自动设置',
        },
        checkPlayingSetting(key) {
            return STORAGE.checkPlayingSetting(key, this.startCheckbox.options[key].status);
        },
        checkPlayerSetting(key) {
            return STORAGE.checkPlayerSetting(key, this.playerCheckbox.options[key].status);
        },
        checkEndedSetting(key) {
            return STORAGE.checkEndedSetting(key, this.endCheckbox.options[key].status);
        },
        trigger(mutation, target) {
            if (target.hasClass('bpx-player-control-bottom-right')) {
                if (mutation.addedNodes[0].className == 'bpx-player-ctrl-btn bpx-player-ctrl-quality') {
                    this.videoQuality();
                } else if (mutation.addedNodes.length == 1 && mutation.addedNodes[0].className.indexOf('bpx-player-ctrl-full') > 0) {
                    this.playStartMode();
                }
            } else if (target.hasClass('bpx-player-state-buff-icon')) {
                UI.rConCss();
            }
        },
        attributesTrigger() {
            this.adjustUI();
        },
        adjustUI() {
            this.checkPlayerSetting('ultraWidescreen') && UI.ultraWidescreenCss();
            if (this.checkPlayerSetting('widescreenScroll2Top') && UI.isWide()) {
                CONTROLLER.scroll2Top();
            } else if (this.checkPlayerSetting('widescreenPlayerSetOnTop') && UI.isWide()) {
                CONTROLLER.playerSetOnTop();
            }
            UI.rConCss();
        },
        init() {
            window.addEventListener('resize', () => {
                this.adjustUI();
            });

            H5_PLAYER.addEventListener('loadeddata', () => {
                this.checkPlayerSetting('hideSenderBar') && UI.hideSenderBar();
                this.checkPlayerSetting('bottomTitle') && UI.bottomTitle();
                this.checkPlayingSetting('moreDescribe') && this.moreDescribe();
                this.playStartMode();
            });
            H5_PLAYER.addEventListener('timeupdate', () => {
                REPEAT_CONTROLLER.check() && REPEAT_CONTROLLER.start();
            });
            H5_PLAYER.addPlayingEvent(() => {
                if (!H5_PLAYER.played) {
                    this.checkPlayingSetting('lightOff') && CONTROLLER.light(OFF);
                }
                this.checkPlayerSetting('lightOffWhenPlaying') && CONTROLLER.light(OFF);
                H5_PLAYER.played = true;
            });

            H5_PLAYER.addPauseEvent(() => {
                this.checkPlayerSetting('lightOnWhenPause') && CONTROLLER.light(ON);
                this.checkPlayerSetting('screenWhenPause') && CONTROLLER.mode(DEFAULT);
            });

            H5_PLAYER.addEndedEvent(() => {
                this.checkEndedSetting('lightOn') && CONTROLLER.light(ON);
                this.checkEndedSetting('autoJumpContent') && CONTROLLER.jumpContent();

                if (this.checkEndedSetting('exitScreen')) {
                    CONTROLLER.mode(DEFAULT);
                } else if (this.checkEndedSetting('exit2WideScreen')) {
                    CONTROLLER.mode(WIDESCREEN);
                }
            });

        },
        playStartMode() {
            if (this.checkPlayingSetting('webFullscreen')) {
                CONTROLLER.mode(WEBFULLSCREEN);
            } else if (this.checkPlayingSetting('widescreen')) {
                CONTROLLER.mode(WIDESCREEN);
            }
        },
        videoQuality() {
            if (!this.checkPlayingSetting('highQuality') && !this.checkPlayingSetting('vipHighQuality')) {
                return;
            }
            const btn = q(SELECTOR.videoQuality);
            let index = -1;
            if (this.checkPlayingSetting('highQuality')) {
                index = btn.findIndex(e => !q(e).find(SELECTOR.vipVideoQuality)[0]);
            } else if (this.checkPlayingSetting('vipHighQualityNot4K')) {
                index = btn.findIndex(e => !e.textContent.includes('4K'))
            }
            index > -1 && btn[index].click();
        },
        moreDescribe() {
            q(SELECTOR.moreDescribe).click();
        }
    };
    const STORAGE = {
        version: 'v1',
        getDefaultShortCutStatus(key, defaultValue) {
            return this.get('defaultShortCut', key, defaultValue);
        },
        getQuickDoKey(quickDoKey, defaultValue) {
            return this.get('quickDo', quickDoKey, defaultValue);
        },
        getControllerConfigStatus(key, defaultValue) {
            return this.get('controllerConfig', key, defaultValue);
        },
        checkPlayingSetting(key, defaultValue) {
            return this.get('startCheckbox', key, defaultValue) === ON;
        },
        checkPlayerSetting(key, defaultValue) {
            return this.get('playerCheckbox', key, defaultValue) === ON;
        },
        checkEndedSetting(key, defaultValue) {
            return this.get('endCheckbox', key, defaultValue) === ON;
        },
        gmKey(configName, key) {
            return `${this.version}:${configName}:${key}`;
        },
        get(configName, key, defaultValue) {
            if (GM_getValue(this.gmKey(configName, key)) == undefined) {
                return defaultValue;
            }
            return GM_getValue(this.gmKey(configName, key));
        },
        save(configName, key, value) {
            GM_setValue(this.gmKey(configName, key), value);
        },
    };
    const SETTING_PANEL = {
        init() {
            this.newSettingPanel();
            ['playerCheckbox', 'startCheckbox', 'endCheckbox'].forEach(configName => {
                for (let [key, { text, status, ban, fn, tips }] of Object.entries(AUTOMATON[configName].options)) {
                    this.addCheckboxSettingItem(configName, key, text, status, ban, fn, tips);
                }
            });
            for (let [key, { value, text }] of Object.entries(CONTROLLER.quickDo)) {
                this.addInputSettingItem('quickDo', key, value, text);
            }
            for (let [key, { text, status, tips }] of Object.entries(CONTROLLER.config)) {
                this.addCheckboxSettingItem('controllerConfig', key, text, status, null, null, null);
            }
            for (let [key, { text, status, bindKey }] of Object.entries(KEY_BOARD.defaultShortCut)) {
                this.addCheckboxSettingItem("defaultShortCut", key, `屏蔽${text}`, status, null, null, null);
            }

        },
        newSettingPanel() {
            const id = 'qd-setting-panel';
            const araeId = id + '-area'
            const colseId = id + '-close'
            const html = `
            <div id='${id}' style="height: 500px;width: 800px;color: #fff;border-radius: 4px;position: absolute;text-align: center;z-index: 80;-webkit-font-smoothing: antialiased;background-color: rgba(33,33,33,.9);left: 50%;top: 50%;-webkit-transform: translate(-50%,-50%);transform: translate(-50%,-50%);">
                <div style="font-size: 16px;text-align: center;line-height: 40px;border-bottom: 1px solid hsla(0,0%,100%,.1);">
                    bilibili H5播放器快捷操作设置 <span id='${colseId}' style="float: right;margin-right: 10px;"> X </span>
                </div>
                <div id='${araeId}' class="bpx-player-hotkey-panel-area" style="touch-action: pan-x; user-select: none; -webkit-user-drag: none; -webkit-tap-highlight-color: rgba(0, 0, 0, 0);min-height: 430px;padding-left: 35px;">
                    <div class="bpx-player-hotkey-panel-content" style="transition-timing-function: cubic-bezier(0.165, 0.84, 0.44, 1); transition-duration: 0ms; transform: translate(0px, 0px) scale(1) translateZ(0px);">
                    </div>
                </div>
            </div>
            `;
            q(SELECTOR.playerVideoArea).append(html);
            this.settingPanel = q(`#${id}`);
            this.settingPanelArea = q(`#${araeId}`);
            q(`#${colseId}`).on('click', () => {
                this.close();
            })
            this.close();
        },
        addCheckboxSettingItem(configName, key, text, status, ban, fn, tips) {
            const checked = STORAGE.get(configName, key, status) == ON ? 'checked' : '';
            const checkboxId = `${configName}-${key}-box`;
            let item = `
            <div style="min-width: 185px;float: left;height: 24px;line-height: 24px;text-align: left;">
                <span>
                <input id="${checkboxId}" type="checkbox" ${checked}></input>
                </span>
                <span>${text}</span>
            </div>`
            this.settingPanelArea.append(item);
            q(`#${checkboxId}`).on('click', function(e) {
                STORAGE.save(configName, key, this.checked ? ON : OFF);
            });
        },
        addInputSettingItem(configName, key, value, text) {
            const inputId = `${configName}-${key}-input`;
            const val = STORAGE.get(configName, key, value);
            let item = `
            <div style="min-width: 185px;float: left;height: 24px;line-height: 24px;text-align: left;">
                <span>
                <input id="${inputId}" type="input" value="${val}" style="width: 35px;height: 12px;color: black;text-align: center;"></input>
                </span>
                <span>${text}</span>
            </div>`
            this.settingPanelArea.append(item);
            const input = q(`#${inputId}`);
            input.on('keydown', e => {
                const isGlobalHotKey = KEY_BOARD.isGlobalHotKey(e.keyCode);
                const key = isGlobalHotKey || e.key.toLowerCase();
                const isA2Z = e.keyCode >= 65 && e.keyCode <= 90;
                const isSymbol = "[]\\;',./-=".indexOf(key) > -1;
                if ((isGlobalHotKey || isA2Z || isSymbol || e.keyCode === KEY_BOARD.keyCode.enter) && KEY_BOARD.getKeyCode(key)) {
                    input.val(key);
                }
                const isDelete = e.keyCode == 8 || e.keyCode == 46;
                (!isDelete || isGlobalHotKey) && e.preventDefault();
            }).on('keyup', e => {
                STORAGE.save(configName, key, input.val());
            }).on('click', e => {
                input.select();
                e.preventDefault();
            });
        },
        show() {
            this.settingPanel.css('display', '');
        },
        close() {
            this.settingPanel.css('display', 'none');
        },
        switch() {
            this.settingPanel.getCss('display') == 'none' ? this.show() : this.close();
        }
    };
    new MutationObserver((mutations, observer) => {
        mutations.forEach(mutation => {
            const target = q(mutation.target);
            if (target.hasClass('fixed-header')) {
                UI.removeFixedHeader();
            }
            if (target.hasClass('header-v2')) {
                if (H5_PLAYER.h5Player) {
                    H5_PLAYER.played = false;
                    return;
                }
                try {
                    H5_PLAYER.init();
                    CONTROLLER.initKeyDown();
                    AUTOMATON.init();
                    SETTING_PANEL.init();
                    console.log('bilibili-quickdo done');
                } catch (e) {
                    console.error('bilibili-quickdo init error:', e);
                }
            } else if (target.hasClass('bpx-player-sending-bar') && mutation.addedNodes.length) {
                CONTROLLER.bindDanmuInputKeydown();
            } else {
                AUTOMATON.trigger(mutation, target);
            }
        });
    }).observe(document.body, {
        childList: true,
        subtree: true,
    });
    new MutationObserver((mutations, observer) => {
        AUTOMATON.attributesTrigger();
    }).observe(document.body, {
        attributes: true,
    });
})();