Greasy Fork

Greasy Fork is available in English.

外挂弹幕插件

为任意网页播放器提供了加载本地弹幕的功能

当前为 2023-08-24 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         外挂弹幕插件
// @version      0.2
// @description  为任意网页播放器提供了加载本地弹幕的功能
// @author       DeltaFlyer
// @copyright    2023, DeltaFlyer(https://github.com/DeltaFlyerW)
// @license      MIT
// @match        https://pan.baidu.com/pfile/video*
// @match        https://www.aliyundrive.com/drive/legacy*
// @match        https://www.tucao.cam/play/*
// @run-at       document-start
// @grant        unsafeWindow
// @grant        GM_setValue
// @grant        GM_getValue
// @icon         https://avatars.githubusercontent.com/u/1879224?v=4
// @require      https://cdn.jsdelivr.net/npm/@xpadev-net/[email protected]/dist/bundle.min.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/danmaku.min.js
// @namespace    http://greasyfork.icu/users/927887
// ==/UserScript==


(async function main() {
    async function waitForDOMContentLoaded() {
        return new Promise((resolve) => {
            console.log(document.readyState)
            if (document.readyState === 'loading') {
                document.addEventListener('DOMContentLoaded', resolve);
            } else {
                resolve();
            }
        });
    }

    async function sleep(time) {
        await new Promise((resolve) => setTimeout(resolve, time));
    }

    await waitForDOMContentLoaded()
    let danmakuPlayer
    let toastText = (function () {
        let html = `
<style>
.df-bubble-container {
    position: fixed;
    bottom: 20px;
    right: 20px;
    z-index: 1000;
    display: block !important;
}

.df-bubble {
    background-color: #333;
    color: white;
    padding: 10px 20px;
    border-radius: 5px;
    margin-bottom: 10px;
    opacity: 0;
    transition: opacity 0.5s ease-in-out;
    max-width: 300px;
    box-shadow: 0px 3px 6px rgba(0, 0, 0, 0.2);
    display: block !important;
}

.df-show-bubble {
    opacity: 1;
}
</style>
<div class="df-bubble-container" id="bubbleContainer"></div>`
        document.body.insertAdjacentHTML("beforeend", html)
        let bubbleContainer = document.querySelector('.df-bubble-container')

        function createToast(text) {
            console.log('toast', text)
            const bubble = document.createElement('div');
            bubble.classList.add('df-bubble');
            bubble.textContent = text;

            bubbleContainer.appendChild(bubble);
            setTimeout(() => {
                bubble.classList.add('df-show-bubble');
                setTimeout(() => {
                    bubble.classList.remove('df-show-bubble');
                    setTimeout(() => {
                        bubbleContainer.removeChild(bubble);
                    }, 500); // Remove the bubble after fade out
                }, 3000); // Show bubble for 3 seconds
            }, 100); // Delay before showing the bubble
        }

        return createToast
    })();
    let loadDanmaku = (function () {
        let [loadNicoCommentArt, clearNicoComment] = (function loadNicoCommentArt() {
            function buildCanvas() {
                // Get a reference to the existing element in the document
                let html = `
<style>
  #nico-canvas,
  #nico-container
   {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    height: 100%;
    width: 100%;
    object-fit: contain;
    pointer-events: none;
    z-index: 999;
  }
</style>
<div id="nico-container">
<canvas id="nico-canvas" width="1920" height="1080""></canvas>
</div>
`
                videoElem.parentElement.insertAdjacentHTML('beforeend', html);
                return videoElem.parentElement.querySelector("#nico-canvas")
            }

            let niconiComments
            let canvasElem
            let interval

            return [async function (comments) {
                if (!niconiComments) {
                    canvasElem = buildCanvas()
                    console.log('buildNicoCanvas', canvasElem)
                    niconiComments = new NiconiComments(canvasElem, [], {
                        mode: 'default',
                        keepCA: true,
                    });
                    interval = setInterval(() => {
                        niconiComments.drawCanvas(Math.floor(videoElem.currentTime * 100))
                    }, 10);
                }
                niconiComments.addComments(...comments)
                console.log('addCommentArt', niconiComments, comments)
            }, function () {
                if (canvasElem) {
                    canvasElem.parentElement.removeChild(canvasElem)
                    clearInterval(interval)
                    niconiComments = undefined
                    interval = undefined
                    canvasElem = undefined
                }
            }];
        })();


        function xmlunEscape(content) {
            return content.replace(';', ';')
                .replace(/&amp;/g, '&')
                .replace(/&lt;/g, '<')
                .replace(/&gt;/g, '>')
                .replace(/&apos;/g, "'")
                .replace(/&quot;/g, '"')
        }

        function findAll(inputString, regex) {
            const matches = [];
            let match;

            while ((match = regex.exec(inputString)) !== null) {
                matches.push(match);
            }
            return matches;
        }

        function xml2danmu(sdanmu) {
            const extraArgRegex = /(\S+?)\s*=\s*"(.*?)"/g
            let ldanmu = findAll(sdanmu, /<d p="(.*?)"(.*?)>(.*?)<\/d>/g);
            for (let i = 0; i < ldanmu.length; i++) {
                let danmu = ldanmu[i]
                let argv = danmu[1].split(',')
                let result = {
                    color: Number(argv[3]),
                    content: xmlunEscape(danmu[3]),
                    ctime: Number(argv[4]),
                    fontsize: Number(argv[2]),
                    id: Number(argv[7]),
                    idStr: argv[7],
                    midHash: argv[6],
                    mode: Number(argv[1]),
                    progress: Math.round(Number(argv[0]) * 1000),
                    weight: 8
                }
                if (danmu[2].length !== 0) {
                    for (let extraArg of findAll(danmu[2], extraArgRegex)) {
                        result[extraArg[1]] = xmlunEscape(extraArg[2])
                    }
                }
                ldanmu[i] = result
            }
            return ldanmu
        }

        let isCommentArt = (function () {
            let caCommands = ['full', 'patissier', 'ender', 'mincho', 'gothic', 'migi', 'hidari', 'shita']
            let caCharRegex = new RegExp(' ◥█◤■◯△×\u05C1\u0E3A\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u200B\u200C\u200D\u200E\u200F\u3000\u3164\u2580\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588\u2589\u258A\u258B\u258C\u258D\u258E\u258F\u2590\u2591\u2592\u2593\u2594\u2595\u2596\u2597\u2598\u2599\u259A\u259B\u259C\u259D\u259E\u259F\u25E2\u25E3\u25E4\u25E5'.split('').join('|'))

            return function (danmu) {
                let command = danmu.mail
                let content = danmu.content
                let isCommentArt = content.split("\n").length > 2;
                if (caCharRegex.exec(content)) {
                    isCommentArt = true
                }
                let lcommand = command.split(' ')
                for (let command of lcommand) {
                    switch (command) {
                        case  'owner': {
                            isCommentArt = true
                            danmu.owner = true
                            break
                        }
                        case caCommands.includes(command): {
                            isCommentArt = true
                            break
                        }
                        case command[0] === "@": {
                            isCommentArt = true
                            break
                        }
                    }
                }
                if (isCommentArt) {
                    return {
                        vpos: Math.round(danmu.progress / 10),
                        date: danmu.time,
                        content: danmu.content,
                        mail: danmu.mail.split(' ')
                    }
                }
            }
        })();

        function intToHexColor(colorInt) {
            const red = (colorInt >> 16) & 0xFF;
            const green = (colorInt >> 8) & 0xFF;
            const blue = colorInt & 0xFF;

            const hex = ((1 << 24) | (red << 16) | (green << 8) | blue).toString(16).slice(1);

            return `#${hex}`;
        }

        async function loadDanmaku(text) {
            let ldanmu = xml2danmu(text)
            console.log(ldanmu)
            toastText(`从文件中读取到${ldanmu.length}条弹幕`)
            let modeDict = {
                1: 'rtl',
                4: 'bottom',
                5: 'top'
            }
            let nicoCommentList = []
            let biliDanmakuList = []

            for (let danmu of ldanmu) {
                if (danmu.mail) {
                    let art = isCommentArt(danmu)
                    if (art) {
                        nicoCommentList.push(art)
                        continue
                    }
                }
                biliDanmakuList.push(
                    {
                        text: danmu.content,
                        time: danmu.progress / 1000,
                        mode: modeDict[danmu.mode],
                        style: {
                            originFontSize: danmu.fontsize,
                            fontSize: danmu.fontsize * currentSetting.scale + 'px',
                            color: intToHexColor(danmu.color),
                            textShadow: '-1px -1px #000, -1px 1px #000, 1px -1px #000, 1px 1px #000'
                        }
                    }
                )
            }
            while (!danmakuPlayer) {
                await sleep(500)
            }
            for (let danmaku of biliDanmakuList) {
                danmakuPlayer.emit(danmaku)
            }
            if (nicoCommentList.length !== 0) {
                loadNicoCommentArt(nicoCommentList)
            }
        }

        return loadDanmaku
    })();


    function getLocalSetting(key) {
        let value = GM_getValue(key)
        console.log('get', key, value)
        if (value) {
            return value
        } else {
            return {}
        }
    }

    function saveLocalSetting(key, value) {
        console.log('save', key, value)
        GM_setValue(key, value)
    }

    (function createFileDropMask() {
        const mask = document.createElement('div');
        mask.id = "danmakuLoaderMask"
        mask.style.position = 'fixed';
        mask.style.top = '0';
        mask.style.left = '0';
        mask.style.width = '100%';
        mask.style.height = '100%';
        mask.style.backgroundColor = 'rgba(0, 0, 0, 0)';
        mask.style.zIndex = '9999';
        mask.style.pointerEvents = 'none';
        mask.style.opacity = '0';

        document.documentElement.insertBefore(mask, document.documentElement.firstChild);

        const handleFileDrop = function (event) {
            event.preventDefault();
            for (let file of event.dataTransfer.files) {
                const reader = new FileReader();
                reader.onload = function (event) {
                    console.log(['File content:', event.target.result]);
                    loadDanmaku(event.target.result)
                };
                reader.readAsText(file);
            }
        };

        document.addEventListener('dragover', function (event) {
            event.preventDefault();
            mask.style.backgroundColor = 'rgba(0, 0, 0, 0.5)';
        });

        document.addEventListener('dragleave', function (event) {
            event.preventDefault();
            mask.style.backgroundColor = 'rgba(0, 0, 0, 0)';
        });

        document.addEventListener('drop', handleFileDrop);
    })();

    let currentOffset = 0

    function updateDanmakuOffset(value) {
        for (let comment of danmakuPlayer.comments) {
            comment.time = comment.time - currentOffset + value
        }
        danmakuPlayer.clear()
        currentOffset = value
        let message
        if (currentOffset < 0) {
            message = `弹幕提前${-currentOffset}秒出现`
        } else if (currentOffset > 0) {
            message = `弹幕延迟${currentOffset}秒出现`
        } else {
            message = '重置弹幕时间'
        }
        toastText(message)
    }

    let settingPanelOptions = [
        {type: 'slider', id: 'speed', label: "弹幕速度", range: [0.5, 1.5], default: 1},
        {type: 'slider', id: 'scale', label: "字体大小", range: [0.5, 1.5], default: 1},
        {type: 'slider', id: 'opacity', label: '不透明度', range: [0, 1], default: 1},
        {
            type: 'row', children: [
                {
                    'type': 'numberInput', 'id': 'danmakuOffset', label: "弹幕延迟", default: 0,
                },
                {
                    'type': 'textSelector',
                    'id': 'maxHeight',
                    label: "显示区域",
                    optionText: ['25%', '50%', '75%', '100%'],
                    optionValue: ['25%', '50%', '75%', '100%'],
                    default: '100%'
                },
            ]
        }
    ]
    let currentSetting = getLocalSetting("danmakuSetting")
    for (let option of settingPanelOptions) {
        if (option.id) {
            if (!currentSetting[option.id]) {
                currentSetting[option.id] = option.default
            }
        } else {
            for (let child of option.children) {
                if (!currentSetting[child.id]) {
                    currentSetting[child.id] = child.default
                }
            }
        }
    }
    let showSettingPanel = (function (settingPanelOptions, changeHandle) {
        let panelStyles = `
        <style>
        body {
            margin: 0;
            padding: 0;
            font-family: Arial, sans-serif;
            overflow: hidden;
        }

        #panel {
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background-color: #333;
            color: #fff;
            padding: 20px;
            border-radius: 5px;
            box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.5);
            z-index: 999999;
        }
        
        .slider-label{
            width: 12ch;
        }

        
    
        .apply-button {
            display: flex;
            justify-content: center;
            align-items: center;
            background-color: #555;
            color: white;
            border: none;
            padding: 8px 12px;
            cursor: pointer;
            border-radius: 4px;
            text-decoration: none;
            margin-top: 10px;
            margin-left: auto;
        }

        .row {
            display: flex;
            align-items: center;
            margin-bottom: 10px;
        }

        .slider {
            flex: 1;
        }

        .slider-value {
            margin-left: 10px;
            font-size: 14px;
            color: #bbb;
            width: 4ch;
        }

        .selector {
            flex: 1;
            margin-right: 10px;
            padding: 5px;
            border: 1px solid #555;
            border-radius: 3px;
            background-color: #444;
            color: #fff;
        }

        .number-input {
            width: 6ch;
            padding: 5px;
            border: 1px solid #555;
            border-radius: 3px;
            background-color: #444;
            color: #fff;
            margin-right: 10px;
            margin-left: 5px;
        }

        .text-selector {
            width: 10ch;
            padding: 5px;
            border: 1px solid #555;
            border-radius: 3px;
            background-color: #444;
            color: #fff;
            margin-left: 5px;
        }

        .equal-row {
            display: flex;
            justify-content: space-between;
            align-items: center;
        }

        .checkbox-group {
            flex: 1;
            display: flex;
            justify-content: center;
        }
    </style>
`

        // Create the setting panel HTML string based on the provided options
        function createPanelHTML(options) {
            let html = '<div id="panel" style="display: none;">'
            options.forEach(option => {
                if (option.type === 'slider') {
                    html += `<div class="row">
                <label class="slider-label" for="${option.id}">${option.label}:</label>
                <input type="range" class="slider" id="${option.id}" min="${option.range[0]}" max="${option.range[1]}" step="0.01" value="${currentSetting[option.id] || option.default}">
                <span class="slider-value" id="${option.id}Value">${currentSetting[option.id] || option.default}</span>
            </div>`;
                } else if (option.type === 'equal-row' || option.type === 'row') {
                    html += `<div class="${option.type}">`;
                    option.children.forEach(child => {
                        // Handle checkboxes
                        if (child.type === 'checkbox') {
                            const checked = currentSetting[child.id] !== undefined ? currentSetting[child.id] : child.default;
                            html += `<div class="checkbox-group">
                                    <label for="${child.id}">${child.label}:</label>
                                    <input type="checkbox" id="${child.id}" ${checked ? 'checked' : ''}>
                                    </div>`;
                        }
                        // Handle number input
                        else if (child.type === 'numberInput') {
                            html += `<label for="${child.id}">${child.label}:</label>
                                    <input type="number" class="number-input" id="${child.id}" value="${currentSetting[child.id] || child.default}">
                                    `;
                        }
                        // Handle text selector
                        else if (child.type === 'textSelector') {
                            let currentValue = currentSetting[child.id] || child.default
                            html += `<label for="${child.id}">${child.label}:</label>
                                    <select class="selector text-selector" id="${child.id}">`;
                            child.optionText.forEach((text, index) => {
                                const value = child.optionValue[index];
                                const selected = currentValue === value ? 'selected' : '';
                                html += `<option value="${value}" ${selected}>${text}</option>`;
                            });
                            html += `</select>`;
                        }
                    });
                    html += `</div>`;
                }
            });
            html += '<button class="apply-button" id="applyButton">应用</button></div>';
            return html;
        }

        function createSettingPanel(settingPanelOptions, changeHandle) {

            document.body.insertAdjacentHTML('beforeend', panelStyles);
            const panelHTML = createPanelHTML(settingPanelOptions);
            document.body.insertAdjacentHTML('beforeend', panelHTML);

            const panel = document.getElementById('panel');

            panel.querySelector('#applyButton').addEventListener('click', () => {
                panel.style.display = 'none';
                saveLocalSetting('danmakuSetting', currentSetting)
            });

            const sliders = panel.querySelectorAll('.slider');
            const sliderValues = panel.querySelectorAll('.slider-value');

            sliders.forEach((slider, index) => {
                slider.addEventListener('input', () => {
                    sliderValues[index].textContent = slider.value;
                    changeHandle[slider.id](parseFloat(slider.value), slider.id);
                });
            });

            // Handle checkbox changes
            const checkboxes = panel.querySelectorAll('input[type="checkbox"]');
            checkboxes.forEach(checkbox => {
                checkbox.addEventListener('change', () => {
                    changeHandle[checkbox.id](Number(checkbox.checked), checkbox.id);
                });
            });

            // Handle number input changes
            const numberInputs = panel.querySelectorAll('.number-input');
            numberInputs.forEach(input => {
                input.addEventListener('input', () => {
                    const value = parseFloat(input.value);
                    if (!isNaN(value)) {
                        changeHandle[input.id](value, input.id);
                    }
                });
            });

            // Handle text selector changes
            const textSelectors = panel.querySelectorAll('.text-selector');
            textSelectors.forEach(selector => {
                selector.addEventListener('change', () => {
                    changeHandle[selector.id](selector.value, selector.id);
                });
            });

            return panel
        }


        let panel = createSettingPanel(settingPanelOptions, changeHandle)
        panel.style.display = 'none'
        return function () {
            if (panel.style.display !== 'block') {
                panel.style.display = 'block'
            } else {
                panel.style.display = 'none'
            }
        }
    })(
        settingPanelOptions,
        {
            speed(value, id) {
                danmakuPlayer.speed = 144 * value;
                currentSetting[id] = value;
            }, scale(value, id) {
                danmakuPlayer.comments.forEach((danmaku) => {
                    danmaku.style.fontSize = danmaku.style.originFontSize * Number(value) + 'px'
                })
                currentSetting[id] = value;
            }, opacity(value, id) {
                setCssVar(id, value);
                currentSetting[id] = value;
            }, maxHeight(value, id) {
                setCssVar(id, value);
                currentSetting[id] = value;
                danmakuPlayer.resize()
            }, danmakuOffset(value) {
                updateDanmakuOffset(value)
            }
        });

    let showKeymapPanel = (function () {
        let html = `<style>
        #keymapPanel {
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background-color: #333;
            color: #fff;
            padding: 20px;
            border-radius: 5px;
            box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.5);
            z-index: 999999;
            display: none;
        }
        #keymapPanel>ul{
        margin-bottom: 30px;
        }
        #keymapPanel>ul>li{
        margin-top: 5px;
        margin-left: 10px;
        margin-right: 10px;
        }
        .close-button {
        position: absolute;
        bottom: 10px;
        right: 10px;
        background-color: #555;
        color: white;
        border: none;
        border-radius: 3px;
        padding: 5px 10px;
        cursor: pointer;
        }
        </style>
        <div id="keymapPanel"><ul>
            <li><span>大写锁定键</span> : 切换弹幕显示</li>
            <li><span>C</span> : 切换弹幕显示</li>
            <li><span>D</span> : 选择弹幕文件</li>
            <li><span>O</span> : 显示/关闭弹幕设置</li>
            <li><span>K</span> : 显示/关闭快捷键说明</li>
            
            <li><span>[</span> : 所有弹幕提前1秒出现</li>
            <li><span>]</span> : 所有弹幕延迟1秒出现</li>
            <li><span>Ctrl+[</span> : 弹幕提前5秒出现</li>
            <li><span>Ctrl+]</span> : 弹幕延迟5秒出现</li>
        </ul>
        <button class="close-button" id="closeKeymapPanelButton">关闭</button>
        </div>`

        document.body.insertAdjacentHTML('beforeend', html)
        let panel = document.getElementById('keymapPanel')
        panel.querySelector('#closeKeymapPanelButton').addEventListener('click', () => {
            panel.style.display = 'none'
        })
        return function () {
            if (panel.style.display !== 'block') {
                panel.style.display = 'block'
            } else {
                panel.style.display = 'none'
            }
        }
    })();

    function selectDanmakuFile() {
        const input = document.createElement('input');
        input.type = 'file';
        input.accept = "application/xml"
        return new Promise((resolve, reject) => {
            input.addEventListener('change', (event) => {
                for (let file of event.target.files) {
                    const reader = new FileReader();
                    reader.onload = function (event) {
                        console.log(['File content:', event.target.result]);
                        loadDanmaku(event.target.result)
                    };
                    reader.readAsText(file);
                }
                resolve()
            });
            input.click();
        });
    }

    (function applyKeymap(keycodeMap) {
        document.addEventListener('keydown', (event) => {
            const keysPressed = [];

            if (event.ctrlKey) {
                keysPressed.push('17');  // Ctrl key
            }
            if (event.altKey) {
                keysPressed.push('18');  // Alt key
            }
            if (event.shiftKey) {
                keysPressed.push('16');  // Shift key
            }

            keysPressed.push(event.keyCode.toString());

            const combinedKey = keysPressed.join('+');
            console.log('combinedKey', combinedKey)
            const action = keycodeMap[combinedKey];

            if (action) {
                action();
            }
        });
    })({
        '20': function () {
            danmakuPlayer.switch()
        },
        '55': function () {
            danmakuPlayer.switch()
        },
        '219': function () {
            updateDanmakuOffset(currentOffset - 1)
        },
        '221': function () {
            updateDanmakuOffset(currentOffset + 1)
        },
        '17+221': function () {
            updateDanmakuOffset(currentOffset + 5)
        },
        '17+219': function () {
            updateDanmakuOffset(currentOffset - 5)
        },
        '79': function () {
            showSettingPanel()
        },
        '68': function () {
            selectDanmakuFile()
        },
        '75': function () {
            showKeymapPanel()
        }
    });


    let setCssVar = function (vars) {
        let styleTag = document.createElement('style');
        document.head.appendChild(styleTag);
        const root = document.documentElement;

        // Generate the CSS variable declarations based on the provided variables
        let cssText = ':root {\n';
        for (const [name, value] of Object.entries(vars)) {
            cssText += `    --${name}: ${value};\n`;
        }
        cssText += '}\n';

        styleTag.innerHTML = cssText; // Apply the CSS variables

        return function updateCssVar(name, value) {
            root.style.setProperty(`--${name}`, value);
        };
    }({
        'opacity': currentSetting['opacity'],
        'maxHeight': currentSetting['maxHeight'],
    });
    (function createToolbar(config) {
        let html = `
<style>
  body {
    margin: 0;
    padding: 0;
    font-family: Arial, sans-serif;
    overflow: hidden;
  }

  #triggerArea {
    position: fixed;
    top: 10%;
    left: 0;
    width: 10%;
    height: 80%;
    cursor: pointer;
  }

  #toolbar {
    position: fixed;
    top: 50%;
    left: -250px;
    transform: translateY(-50%);
    background-color: #333;
    color: #fff;
    padding: 10px;
    border-top-right-radius: 5px;
    border-bottom-right-radius: 5px;
    cursor: grab;
    transition: left 0.3s;
    z-index: 999999;
  }

  #toolbar:active {
    cursor: grabbing;
  }

  #toolbar button {
    display: block;
    margin: 5px 0;
    padding: 8px;
    background-color: #555;
    border: none;
    color: #fff;
    cursor: pointer;
    border-radius: 3px;
  }
</style>
<div id="triggerArea"></div>
<div id="toolbar"></div>
`


        document.body.insertAdjacentHTML('beforeend', html)
        const triggerArea = document.getElementById('triggerArea');
        const toolbar = document.getElementById('toolbar');
        let isDragging = false;
        let isExpanded = false;
        let startY = 0;
        let initialTop = 0;
        let currentSetting = getLocalSetting('dfToolbar')
        if (currentSetting['offsetTopPercent']) {
            toolbar.offsetTop = currentSetting['offsetTopPercent'] * window.innerHeight
        }
        console.log('createToolbar', config)
        for (let option of Object.keys(config.options)) {
            let button = document.createElement("button")
            button.innerText = option
            button.addEventListener('click', config.options[option])
            toolbar.appendChild(button)
        }


        function expandToolbar() {
            if (!isExpanded) {
                toolbar.style.left = '0';
                isExpanded = true;
            }
        }

        function collapseToolbar() {
            if (isExpanded) {
                toolbar.style.left = '-250px';
                isExpanded = false;
            }
        }

        triggerArea.addEventListener('mouseenter', () => {
            expandToolbar();
        });

        triggerArea.addEventListener('mouseleave', () => {
            collapseToolbar();
            if (isDragging) {
                isDragging = false
                dragEndHandle()
            }
        });

        toolbar.addEventListener('mouseenter', () => {
            expandToolbar();
        });

        toolbar.addEventListener('mouseleave', () => {
            if (!isDragging) {
                collapseToolbar();
            }
        });

        toolbar.addEventListener('mousedown', (e) => {
            if (e.target === toolbar) {
                console.log(e.type, e)
                isDragging = true;
                startY = e.clientY;
                initialTop = toolbar.offsetTop;
            }
        });


        let draggingHandle = (e) => {
            if (!isDragging) return;
            const deltaY = e.clientY - startY;
            toolbar.style.top = `${initialTop + deltaY}px`;
        }

        let dragEndHandle = (e) => {
            if (isDragging) {
                isDragging = false;
                currentSetting.offsetTopPercent = toolbar.offsetTop / window.innerHeight
                saveLocalSetting('dfToolbar', currentSetting)
            }
        }

        window.addEventListener('mousemove', draggingHandle);
        window.addEventListener('mouseup', dragEndHandle);

        expandToolbar()
        setTimeout(collapseToolbar, 3000)
    })({
        options: {
            "加载弹幕(D)": selectDanmakuFile,
            "弹幕选项(O)": showSettingPanel,
            "快捷键(K)": showKeymapPanel
        }
    });


    let videoElem


    function createDanmakuPlayer(videoElem) {
        function buildContainer(videoElem) {
            // Get a reference to the existing element in the document
            let html = `
<style>
  #bottom-danmaku-container
   {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    height:  100%;
    width: 100%;
    object-fit: contain;
    pointer-events: none;
    z-index: 999;
    line-height: 1.2;
    opacity: var(--opacity);
  }
  #danmaku-container
   {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    height: var(--maxHeight);
    width: 100%;
    object-fit: contain;
    pointer-events: none;
    z-index: 999;
    line-height: 1.2;
    opacity: var(--opacity);
  }
</style>
<div id="bottom-danmaku-container"></div>
<div id="danmaku-container"></div>
`
            videoElem.parentElement.insertAdjacentHTML('beforeend', html);
            return [videoElem.parentElement.querySelector("#danmaku-container"), videoElem.parentElement.querySelector("#bottom-danmaku-container")]
        }

        let [danmakuContainer, bottomContainer] = buildContainer(videoElem)
        let bottomDanmaku = new Danmaku({
            container: bottomContainer,
            media: videoElem,
            comments: [],
            speed: currentSetting.speed * 144
        })
        let danmaku = new Danmaku({
            container: danmakuContainer,
            media: videoElem,
            comments: [],
            speed: currentSetting.speed * 144
        })
        let player = {
            engines: [bottomDanmaku, danmaku],
            bottomDanmaku: bottomDanmaku,
            danmaku: danmaku,
            shown: true,
            emit(comment) {
                if (comment.mode === 'bottom') {
                    this.bottomDanmaku.emit(comment)
                } else {
                    this.danmaku.emit(comment)
                }
            },
            clear() {
                this.engines.forEach((it) => (it.clear()))
            },
            resize() {
                this.engines.forEach((it) => (it.resize()))
            },
            destroy() {
                this.engines.forEach((it) => (it.destroy()))
            },
            show() {
                this.engines.forEach((it) => (it.show()))
            },
            hide() {
                this.engines.forEach((it) => (it.hide()))
            },
            switch() {
                if (this.shown) {
                    this.hide()
                    this.shown = false
                    toastText("弹幕已隐藏")
                } else {
                    this.show()
                    this.shown = true
                    toastText("弹幕已显示")
                }
            }
        }
        Object.defineProperty(player, 'comments', {
            get() {
                return player.danmaku.comments.concat(player.bottomDanmaku.comments)
            }
        })
        Object.defineProperty(player, 'speed', {
            set(value) {
                this.engines.forEach((it) => (it.speed = value))
            }
        })
        return player
    }

    async function startHook() {
        videoElem = null
        danmakuPlayer = null
        while (!videoElem) {
            let videos = document.querySelectorAll('video')
            for (let videoElement of videos) {
                if (!videoElement.paused) {
                    videoElem = videoElement
                    console.log(videoElement, videos, videoElement.paused)
                }
            }
            await sleep(500)
        }
        danmakuPlayer = createDanmakuPlayer(videoElem)
        toastText("danmakuPlayer initialed")
        console.log("danmakuPlayer inited", danmakuPlayer)

        let lastWidth = videoElem.offsetWidth
        unsafeWindow.danmaku = danmakuPlayer

        while (true) {
            if (videoElem.offsetWidth !== lastWidth) {
                console.log(lastWidth, videoElem.offsetWidth)
                if (videoElem.offsetWidth !== 0) {
                    danmakuPlayer.resize()
                    lastWidth = videoElem.offsetWidth
                } else {
                    danmakuPlayer.destroy()
                    toastText("danmakuPlayer destroyed")
                    break
                }
            }
            await sleep(500)
        }
    }

    while (true) {
        await startHook()
    }
})
()