Greasy Fork is available in English.

Emby danmaku optimized

Emby弹幕插件 优化版

// ==UserScript==
// @name         Emby danmaku optimized
// @description  Emby弹幕插件 优化版
// @grant        none
// @match        */web/index.html
// @version 0.0.1.20240222181856
// @namespace https://greasyfork.org/users/1265529
// ==/UserScript==

(async function () {
    'use strict';
    if (document.querySelector('meta[name="application-name"]').content == 'Emby' || document.documentElement.classList.contains('accent-emby')) {
        // ------ configs start------
        const check_interval = 200;
        const chConverTtitle = ['当前状态: 未启用', '当前状态: 转换为简体', '当前状态: 转换为繁体'];
        const danmuCache = {};
        const LOAD_TYPE = {
            CHECK: 'check',
            INIT: 'init',
            REFRESH: 'refresh',
            RELOAD: 'reload',
            SEARCH: 'search',
        };
        // 0:当前状态关闭 1:当前状态打开
        const danmaku_icons = ['\uE0B9', '\uE7A2'];
        const search_icon = '\uE881';
        const translate_icon = '\uE927';
        const info_icon = '\uE0E0';
        const filter_icons = ['\uE3E0', '\uE3D0', '\uE3D1', '\uE3D2'];
        const danmuStyleSetter_icon = '\uE0F0';
        const buttonOptions = {
            class: 'paper-icon-button-light',
            is: 'paper-icon-button-light',
        };
        const uiAnchorStr = '\uE034';
        const mediaContainerQueryStr = ".graphicContentContainer";
        const mediaQueryStr = 'video';
        const displayButtonOpts = {
            title: '弹幕开关',
            id: 'displayDanmaku',
            innerText: null,
            onclick: () => {
                if (window.ede.loading) {
                    console.log('正在加载,请稍后再试');
                    return;
                }
                console.log('切换弹幕开关');
                window.ede.danmakuSwitch = (window.ede.danmakuSwitch + 1) % 2;
                window.localStorage.setItem('danmakuSwitch', window.ede.danmakuSwitch);
                document.querySelector('#displayDanmaku').children[0].innerText = danmaku_icons[window.ede.danmakuSwitch];
                if (window.ede.danmaku) {
                    window.ede.danmakuSwitch == 1 ? window.ede.danmaku.show() : window.ede.danmaku.hide();
                }
            },
        };
        const searchButtonOpts = {
            title: '搜索弹幕',
            id: 'searchDanmaku',
            innerText: search_icon,
            onclick: () => {
                if (window.ede.loading) {
                    console.log('正在加载,请稍后再试');
                    return;
                }
                console.log('手动匹配弹幕');
                loadDanmaku(LOAD_TYPE.SEARCH);
            },
        };
        const translateButtonOpts = {
            title: null,
            id: 'translateDanmaku',
            innerText: translate_icon,
            onclick: () => {
                if (window.ede.loading) {
                    console.log('正在加载,请稍后再试');
                    return;
                }
                console.log('切换简繁转换');
                window.ede.chConvert = (window.ede.chConvert + 1) % 3;
                window.localStorage.setItem('chConvert', window.ede.chConvert);
                document.querySelector('#translateDanmaku').setAttribute('title', chConverTtitle[window.ede.chConvert]);
                loadDanmaku(LOAD_TYPE.REFRESH);
                console.log(document.querySelector('#translateDanmaku').getAttribute('title'));
            },
        };
        const infoButtonOpts = {
            title: '弹幕信息',
            id: 'printDanmakuInfo',
            innerText: info_icon,
            onclick: () => {
                if (!window.ede.episode_info || window.ede.loading) {
                    console.log('正在加载,请稍后再试');
                    return;
                }
                console.log('显示当前信息');
                let msg = '动画名称:' + window.ede.episode_info.animeTitle;
                if (window.ede.episode_info.episodeTitle) {
                    msg += '\n分集名称:' + window.ede.episode_info.episodeTitle;
                }
                sendNotification('当前弹幕匹配', msg);
            },
        };

        const filterButtonOpts = {
            title: '过滤等级(下次加载生效)',
            id: 'filteringDanmaku',
            innerText: null,
            onclick: () => {
                console.log('切换弹幕过滤等级');
                let level = window.localStorage.getItem('danmakuFilterLevel');
                level = ((level ? parseInt(level) : 0) + 1) % 4;
                window.localStorage.setItem('danmakuFilterLevel', level);
                document.querySelector('#filteringDanmaku').children[0].innerText = filter_icons[level];
            },
        };
        // ------ configs end------
        /* eslint-disable */
        /* https://cdn.jsdelivr.net/npm/danmaku/dist/danmaku.min.js */
        // prettier-ignore
        !function (t, e) { "object" == typeof exports && "undefined" != typeof module ? module.exports = e() : "function" == typeof define && define.amd ? define(e) : (t = "undefined" != typeof globalThis ? globalThis : t || self).Danmaku = e() }(this, (function () { "use strict"; var t = function () { for (var t = ["oTransform", "msTransform", "mozTransform", "webkitTransform", "transform"], e = document.createElement("div").style, i = 0; i < t.length; i++)if (t[i] in e) return t[i]; return "transform" }(); function e(t) { var e = document.createElement("div"); if (e.style.cssText = "position:absolute;", "function" == typeof t.render) { var i = t.render(); if (i instanceof HTMLElement) return e.appendChild(i), e } if (e.textContent = t.text, t.style) for (var n in t.style) e.style[n] = t.style[n]; return e } var i = { name: "dom", init: function () { var t = document.createElement("div"); return t.style.cssText = "overflow:hidden;white-space:nowrap;transform:translateZ(0);", t }, clear: function (t) { for (var e = t.lastChild; e;)t.removeChild(e), e = t.lastChild }, resize: function (t, e, i) { t.style.width = e + "px", t.style.height = i + "px" }, framing: function () { }, setup: function (t, i) { var n = document.createDocumentFragment(), s = 0, r = null; for (s = 0; s < i.length; s++)(r = i[s]).node = r.node || e(r), n.appendChild(r.node); for (i.length && t.appendChild(n), s = 0; s < i.length; s++)(r = i[s]).width = r.width || r.node.offsetWidth, r.height = r.height || r.node.offsetHeight }, render: function (e, i) { i.node.style[t] = "translate(" + i.x + "px," + i.y + "px)" }, remove: function (t, e) { t.removeChild(e.node), this.media || (e.node = null) } }; const n = window.devicePixelRatio || 1; var s = Object.create(null); function r(t, e) { if ("function" == typeof t.render) { var i = t.render(); if (i instanceof HTMLCanvasElement) return t.width = i.width, t.height = i.height, i } var r = document.createElement("canvas"), h = r.getContext("2d"), o = t.style || {}; o.font = o.font || "10px sans-serif", o.textBaseline = o.textBaseline || "bottom"; var a = 1 * o.lineWidth; for (var d in a = a > 0 && a !== 1 / 0 ? Math.ceil(a) : 1 * !!o.strokeStyle, h.font = o.font, t.width = t.width || Math.max(1, Math.ceil(h.measureText(t.text).width) + 2 * a), t.height = t.height || Math.ceil(function (t, e) { if (s[t]) return s[t]; var i = 12, n = t.match(/(\d+(?:\.\d+)?)(px|%|em|rem)(?:\s*\/\s*(\d+(?:\.\d+)?)(px|%|em|rem)?)?/); if (n) { var r = 1 * n[1] || 10, h = n[2], o = 1 * n[3] || 1.2, a = n[4]; "%" === h && (r *= e.container / 100), "em" === h && (r *= e.container), "rem" === h && (r *= e.root), "px" === a && (i = o), "%" === a && (i = r * o / 100), "em" === a && (i = r * o), "rem" === a && (i = e.root * o), void 0 === a && (i = r * o) } return s[t] = i, i }(o.font, e)) + 2 * a, r.width = t.width * n, r.height = t.height * n, h.scale(n, n), o) h[d] = o[d]; var u = 0; switch (o.textBaseline) { case "top": case "hanging": u = a; break; case "middle": u = t.height >> 1; break; default: u = t.height - a }return o.strokeStyle && h.strokeText(t.text, a, u), h.fillText(t.text, a, u), r } function h(t) { return 1 * window.getComputedStyle(t, null).getPropertyValue("font-size").match(/(.+)px/)[1] } var o = { name: "canvas", init: function (t) { var e = document.createElement("canvas"); return e.context = e.getContext("2d"), e._fontSize = { root: h(document.getElementsByTagName("html")[0]), container: h(t) }, e }, clear: function (t, e) { t.context.clearRect(0, 0, t.width, t.height); for (var i = 0; i < e.length; i++)e[i].canvas = null }, resize: function (t, e, i) { t.width = e * n, t.height = i * n, t.style.width = e + "px", t.style.height = i + "px" }, framing: function (t) { t.context.clearRect(0, 0, t.width, t.height) }, setup: function (t, e) { for (var i = 0; i < e.length; i++) { var n = e[i]; n.canvas = r(n, t._fontSize) } }, render: function (t, e) { t.context.drawImage(e.canvas, e.x * n, e.y * n) }, remove: function (t, e) { e.canvas = null } }; function a(t) { var e = this, i = this.media ? this.media.currentTime : Date.now() / 1e3, n = this.media ? this.media.playbackRate : 1; function s(t, s) { if ("top" === s.mode || "bottom" === s.mode) return i - t.time < e._.duration; var r = (e._.width + t.width) * (i - t.time) * n / e._.duration; if (t.width > r) return !0; var h = e._.duration + t.time - i, o = e._.width + s.width, a = e.media ? s.time : s._utc, d = o * (i - a) * n / e._.duration, u = e._.width - d; return h > e._.duration * u / (e._.width + s.width) } for (var r = this._.space[t.mode], h = 0, o = 0, a = 1; a < r.length; a++) { var d = r[a], u = t.height; if ("top" !== t.mode && "bottom" !== t.mode || (u += d.height), d.range - d.height - r[h].range >= u) { o = a; break } s(d, t) && (h = a) } var m = r[h].range, c = { range: m + t.height, time: this.media ? t.time : t._utc, width: t.width, height: t.height }; return r.splice(h + 1, o - h - 1, c), "bottom" === t.mode ? this._.height - t.height - m % this._.height : m % (this._.height - t.height) } var d = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || function (t) { return setTimeout(t, 50 / 3) }, u = window.cancelAnimationFrame || window.mozCancelAnimationFrame || window.webkitCancelAnimationFrame || clearTimeout; function m(t, e, i) { for (var n = 0, s = 0, r = t.length; s < r - 1;)i >= t[n = s + r >> 1][e] ? s = n : r = n; return t[s] && i < t[s][e] ? s : r } function c(t) { return /^(ltr|top|bottom)$/i.test(t) ? t.toLowerCase() : "rtl" } function l() { var t = 9007199254740991; return [{ range: 0, time: -t, width: t, height: 0 }, { range: t, time: t, width: 0, height: 0 }] } function f(t) { t.ltr = l(), t.rtl = l(), t.top = l(), t.bottom = l() } function p() { if (!this._.visible || !this._.paused) return this; if (this._.paused = !1, this.media) for (var t = 0; t < this._.runningList.length; t++) { var e = this._.runningList[t]; e._utc = Date.now() / 1e3 - (this.media.currentTime - e.time) } var i = this, n = function (t, e, i, n) { return function () { t(this._.stage); var s = Date.now() / 1e3, r = this.media ? this.media.currentTime : s, h = this.media ? this.media.playbackRate : 1, o = null, d = 0, u = 0; for (u = this._.runningList.length - 1; u >= 0; u--)o = this._.runningList[u], r - (d = this.media ? o.time : o._utc) > this._.duration && (n(this._.stage, o), this._.runningList.splice(u, 1)); for (var m = []; this._.position < this.comments.length && (o = this.comments[this._.position], !((d = this.media ? o.time : o._utc) >= r));)r - d > this._.duration || (this.media && (o._utc = s - (this.media.currentTime - o.time)), m.push(o)), ++this._.position; for (e(this._.stage, m), u = 0; u < m.length; u++)(o = m[u]).y = a.call(this, o), this._.runningList.push(o); for (u = 0; u < this._.runningList.length; u++) { o = this._.runningList[u]; var c = (this._.width + o.width) * (s - o._utc) * h / this._.duration; "ltr" === o.mode && (o.x = c - o.width + .5 | 0), "rtl" === o.mode && (o.x = this._.width - c + .5 | 0), "top" !== o.mode && "bottom" !== o.mode || (o.x = this._.width - o.width >> 1), i(this._.stage, o) } } }(this._.engine.framing.bind(this), this._.engine.setup.bind(this), this._.engine.render.bind(this), this._.engine.remove.bind(this)); return this._.requestID = d((function t() { n.call(i), i._.requestID = d(t) })), this } function g() { return !this._.visible || this._.paused || (this._.paused = !0, u(this._.requestID), this._.requestID = 0), this } function _() { if (!this.media) return this; this.clear(), f(this._.space); var t = m(this.comments, "time", this.media.currentTime); return this._.position = Math.max(0, t - 1), this } function v(t) { t.play = p.bind(this), t.pause = g.bind(this), t.seeking = _.bind(this), this.media.addEventListener("play", t.play), this.media.addEventListener("pause", t.pause), this.media.addEventListener("playing", t.play), this.media.addEventListener("waiting", t.pause), this.media.addEventListener("seeking", t.seeking) } function w(t) { this.media.removeEventListener("play", t.play), this.media.removeEventListener("pause", t.pause), this.media.removeEventListener("playing", t.play), this.media.removeEventListener("waiting", t.pause), this.media.removeEventListener("seeking", t.seeking), t.play = null, t.pause = null, t.seeking = null } function y(t) { this._ = {}, this.container = t.container || document.createElement("div"), this.media = t.media, this._.visible = !0, this.engine = (t.engine || "DOM").toLowerCase(), this._.engine = "canvas" === this.engine ? o : i, this._.requestID = 0, this._.speed = Math.max(0, t.speed) || 144, this._.duration = 4, this.comments = t.comments || [], this.comments.sort((function (t, e) { return t.time - e.time })); for (var e = 0; e < this.comments.length; e++)this.comments[e].mode = c(this.comments[e].mode); return this._.runningList = [], this._.position = 0, this._.paused = !0, this.media && (this._.listener = {}, v.call(this, this._.listener)), this._.stage = this._.engine.init(this.container), this._.stage.style.cssText += "position:relative;pointer-events:none;", this.resize(), this.container.appendChild(this._.stage), this._.space = {}, f(this._.space), this.media && this.media.paused || (_.call(this), p.call(this)), this } function x() { if (!this.container) return this; for (var t in g.call(this), this.clear(), this.container.removeChild(this._.stage), this.media && w.call(this, this._.listener), this) Object.prototype.hasOwnProperty.call(this, t) && (this[t] = null); return this } var b = ["mode", "time", "text", "render", "style"]; function L(t) { if (!t || "[object Object]" !== Object.prototype.toString.call(t)) return this; for (var e = {}, i = 0; i < b.length; i++)void 0 !== t[b[i]] && (e[b[i]] = t[b[i]]); if (e.text = (e.text || "").toString(), e.mode = c(e.mode), e._utc = Date.now() / 1e3, this.media) { var n = 0; void 0 === e.time ? (e.time = this.media.currentTime, n = this._.position) : (n = m(this.comments, "time", e.time)) < this._.position && (this._.position += 1), this.comments.splice(n, 0, e) } else this.comments.push(e); return this } function T() { return this._.visible ? this : (this._.visible = !0, this.media && this.media.paused || (_.call(this), p.call(this)), this) } function E() { return this._.visible ? (g.call(this), this.clear(), this._.visible = !1, this) : this } function k() { return this._.engine.clear(this._.stage, this._.runningList), this._.runningList = [], this } function C() { return this._.width = this.container.offsetWidth, this._.height = this.container.offsetHeight, this._.engine.resize(this._.stage, this._.width, this._.height), this._.duration = this._.width / this._.speed, this } var D = { get: function () { return this._.speed }, set: function (t) { return "number" != typeof t || isNaN(t) || !isFinite(t) || t <= 0 ? this._.speed : (this._.speed = t, this._.width && (this._.duration = this._.width / t), t) } }; function z(t) { t && y.call(this, t) } return z.prototype.destroy = function () { return x.call(this) }, z.prototype.emit = function (t) { return L.call(this, t) }, z.prototype.show = function () { return T.call(this) }, z.prototype.hide = function () { return E.call(this) }, z.prototype.clear = function () { return k.call(this) }, z.prototype.resize = function () { return C.call(this) }, Object.defineProperty(z.prototype, "speed", D), z }));
        /* eslint-enable */

        class EDE {
            constructor() {
                this.chConvert = 1;
                if (window.localStorage.getItem('chConvert')) {
                    this.chConvert = window.localStorage.getItem('chConvert');
                }
                // 0:当前状态关闭 1:当前状态打开
                this.danmakuSwitch = 1;
                if (window.localStorage.getItem('danmakuSwitch')) {
                    this.danmakuSwitch = parseInt(window.localStorage.getItem('danmakuSwitch'));
                }
                this.danmaku = null;
                this.episode_info = null;
                this.ob = null;
                this.loading = false;
            }
        }

        function createButton(opt) {
            let button = document.createElement('button', buttonOptions);
            button.setAttribute('title', opt.title);
            button.setAttribute('id', opt.id);
            let icon = document.createElement('span');
            icon.className = 'md-icon';
            icon.innerText = opt.innerText;
            button.appendChild(icon);
            button.onclick = opt.onclick;
            return button;
        }

        /** 调整字幕大小和透明度的Button
         * @returns
         */
        function createResizeButton() {
            // 创建按钮
            let button = document.createElement('button', buttonOptions);
            button.setAttribute('title', '弹幕调整');
            button.setAttribute('id', 'danmuStyleSetter');
            //创建按钮图标
            let icon = document.createElement('span');
            icon.className = 'md-icon';
            icon.innerText = danmuStyleSetter_icon;

            // 创建弹出面板
            let popDiv = document.getElementById('danmuStyleSetterPopPanel');
            if (popDiv) popDiv.remove();
            popDiv = document.createElement('div');
            popDiv.setAttribute('id', 'danmuStyleSetterPopPanel');
            popDiv.startSetter = undefined;
            popDiv.style.display = 'none';
            popDiv.style.position = 'absolute';
            popDiv.style.padding = '6px';
            popDiv.style.background = '#555';
            popDiv.style.flexWrap = 'nowrap';
            popDiv.style.justifyContent = 'center';
            popDiv.style.alignItems = 'center';

            // 大小调整SubPanel
            let fontSizeSetPopDiv = document.createElement('div');
            fontSizeSetPopDiv.style.display = 'flex';
            fontSizeSetPopDiv.style.flexDirection = 'column';
            fontSizeSetPopDiv.style.flexWrap = 'nowrap';
            fontSizeSetPopDiv.style.justifyContent = 'center';
            fontSizeSetPopDiv.style.alignItems = 'center';
            // 滑动条
            let fontSizeSlider = document.createElement('input');
            fontSizeSlider.setAttribute('orient', 'vertical');
            fontSizeSlider.setAttribute('type', 'range');
            fontSizeSlider.setAttribute('min', '0.1');
            fontSizeSlider.setAttribute('max', '3');
            fontSizeSlider.setAttribute('step', '0.1');
            fontSizeSlider.style.width = '30px';
            fontSizeSlider.style.height = '60px';
            fontSizeSlider.style.appearance = 'slider-vertical';
            fontSizeSlider.oninput = () => {
                fontSizeInput.value = parseFloat(fontSizeSlider.value).toFixed(1);
            };
            // 输入框
            let fontSizeInput = document.createElement('input');
            fontSizeInput.type = 'text';
            fontSizeInput.style.width = '30px';
            fontSizeInput.style.textAlign = 'center';
            fontSizeInput.oninput = () => {
                var oldValue = window.localStorage.getItem('danmakuFontSizeMagnification') ?? '1.0';
                var fontSizeMagnification = parseFloat(fontSizeInput.value);
                if (isNaN(fontSizeMagnification)) {
                    fontSizeInput.value = oldValue;
                    fontSizeSlider.value = oldValue;
                    alert('请输入有效的数字!');
                } else if (fontSizeMagnification < 0.1 || fontSizeMagnification > 3) {
                    fontSizeInput.value = oldValue;
                    fontSizeSlider.value = oldValue;
                    alert('请输入0.1到3之间的数字!');
                } else {
                    fontSizeSlider.value = fontSizeMagnification.toFixed(1);
                }
            };
            // 标签
            let fontSizeLabel = document.createElement('span');
            fontSizeLabel.innerText = '大小';

            // 透明度调整SubPanel
            let fontOpacitySetPopDiv = document.createElement('div');
            fontOpacitySetPopDiv.style.display = 'flex';
            fontOpacitySetPopDiv.style.flexDirection = 'column';
            fontOpacitySetPopDiv.style.flexWrap = 'nowrap';
            fontOpacitySetPopDiv.style.justifyContent = 'center';
            fontOpacitySetPopDiv.style.alignItems = 'center';
            // 滑动条
            let fontOpacitySlider = document.createElement('input');
            fontOpacitySlider.setAttribute('orient', 'vertical');
            fontOpacitySlider.setAttribute('type', 'range');
            fontOpacitySlider.setAttribute('min', '0.1');
            fontOpacitySlider.setAttribute('max', '1');
            fontOpacitySlider.setAttribute('step', '0.1');
            fontOpacitySlider.style.width = '30px';
            fontOpacitySlider.style.height = '60px';
            fontOpacitySlider.style.appearance = 'slider-vertical';
            fontOpacitySlider.oninput = () => {
                fontOpacityInput.value = parseFloat(fontOpacitySlider.value).toFixed(1);
            };
            // 输入框
            let fontOpacityInput = document.createElement('input');
            fontOpacityInput.type = 'text';
            fontOpacityInput.style.width = '30px';
            fontOpacityInput.style.textAlign = 'center';
            fontOpacityInput.oninput = () => {
                var oldValue = window.localStorage.getItem('danmakuFontOpacity') ?? '1.0';
                var fontOpacity = parseFloat(fontOpacityInput.value);
                if (isNaN(fontOpacity)) {
                    fontOpacityInput.value = oldValue;
                    fontOpacitySlider.value = oldValue;
                    alert('请输入有效的数字!');
                } else if (fontOpacity < 0 || fontOpacity > 1) {
                    fontOpacityInput.value = oldValue;
                    fontOpacitySlider.value = oldValue;
                    alert('请输入0到1之间的数字!');
                } else {
                    fontOpacitySlider.value = fontOpacity.toFixed(1);
                }
            };
            // 标签
            let fontOpacityLabel = document.createElement('span');
            fontOpacityLabel.innerText = '透明度';

            // 监听鼠标点击事件
            button.addEventListener('click', function (event) {
                if (popDiv.style.display == 'none') {
                    // 赋初值
                    let curFontSizeMag = window.localStorage.getItem('danmakuFontSizeMagnification');
                    let curFontOpacity = window.localStorage.getItem('danmakuFontOpacity');
                    curFontSizeMag = isNaN(parseFloat(curFontSizeMag)) ? '1.0' : curFontSizeMag;
                    curFontOpacity = isNaN(parseFloat(curFontOpacity)) ? '1.0' : curFontOpacity;
                    fontSizeSlider.value = curFontSizeMag;
                    fontSizeInput.value = curFontSizeMag;
                    fontOpacitySlider.value = curFontOpacity;
                    fontOpacityInput.value = curFontOpacity;
                    // CSS
                    var x = event.clientX - 36 > 0 ? event.clientX - 36 : 0;
                    var y = event.clientY - 160 > 0 ? event.clientY - 160 : 0;

                    popDiv.startSetter = '1';
                    popDiv.style.left = x + 'px';
                    popDiv.style.top = y + 'px';
                    popDiv.style.display = 'flex';
                } else {
                    popDiv.style.display = 'none';
                    if (popDiv.startSetter) {
                        popDiv.startSetter = undefined;
                        doFontResizeAndOpacityChange(fontSizeInput.value, fontOpacityInput.value);
                    }
                }
            });
            // 监听页面点击事件
            document.addEventListener('click', function (event) {
                // 检查点击事件的目标元素是否是div或div内的元素
                if (event.target === popDiv || popDiv.contains(event.target) || event.target === button || button.contains(event.target)) {
                    return; // 如果是,则不执行下面的代码
                }
                popDiv.style.display = 'none';
                if (popDiv.startSetter) {
                    popDiv.startSetter = undefined;
                    doFontResizeAndOpacityChange(fontSizeInput.value, fontOpacityInput.value);
                }
            });
            // 监听Input回车结束设置
            fontSizeInput.addEventListener('keydown', function (e) {
                if (e.key === 'Enter') {
                    popDiv.style.display = 'none';
                    if (popDiv.startSetter) {
                        popDiv.startSetter = undefined;
                        doFontResizeAndOpacityChange(fontSizeInput.value, fontOpacityInput.value);
                    }
                }
            });
            fontOpacityInput.addEventListener('keydown', function (e) {
                if (e.key === 'Enter') {
                    popDiv.style.display = 'none';
                    if (popDiv.startSetter) {
                        popDiv.startSetter = undefined;
                        doFontResizeAndOpacityChange(fontSizeInput.value, fontOpacityInput.value);
                    }
                }
            });

            // icon插入button
            button.appendChild(icon);
            // 将滑动条和输入框添加到pop
            fontSizeSetPopDiv.appendChild(fontSizeSlider);
            fontSizeSetPopDiv.appendChild(fontSizeInput);
            fontSizeSetPopDiv.appendChild(fontSizeLabel);
            fontOpacitySetPopDiv.appendChild(fontOpacitySlider);
            fontOpacitySetPopDiv.appendChild(fontOpacityInput);
            fontOpacitySetPopDiv.appendChild(fontOpacityLabel);
            popDiv.appendChild(fontSizeSetPopDiv);
            popDiv.appendChild(fontOpacitySetPopDiv);
            // 将POP插入body
            document.body.appendChild(popDiv);

            return button;
        }
        /** 执行弹幕大小和透明度变换 */
        function doFontResizeAndOpacityChange(fontSize, fontOpacity) {
            let oldFontSize = window.localStorage.getItem('danmakuFontSizeMagnification');
            let oldFontOpacity = window.localStorage.getItem('danmakuFontOpacity');
            if (fontSize != oldFontSize || fontOpacity != oldFontOpacity) {
                window.localStorage.setItem('danmakuFontSizeMagnification', fontSize);
                window.localStorage.setItem('danmakuFontOpacity', fontOpacity);
                loadDanmaku(LOAD_TYPE.RELOAD);
            }
        }

        function initListener() {
            let container = document.querySelector(mediaQueryStr);
            // 页面未加载
            if (!container) {
                if (window.ede.episode_info) {
                    window.ede.episode_info = null;
                }
                return;
            }
            if (!container.getAttribute('ede_listening')) {
                console.log('正在初始化Listener');
                container.setAttribute('ede_listening', true);
                container.addEventListener('play', loadDanmaku);
                console.log('Listener初始化完成');
            }
        }

        function getElementsByInnerText(tagType, innerStr, excludeChildNode = true) {
            var temp = [];
            var elements = document.getElementsByTagName(tagType);
            if (!elements || 0 == elements.length) {
                return temp;
            }
            for (let index = 0; index < elements.length; index++) {
                var e = elements[index];
                if (e.innerText.includes(innerStr)) {
                    temp.push(e);
                }
            }
            if (!excludeChildNode) {
                return temp;
            }
            var res = [];
            temp.forEach((e) => {
                var e_copy = e.cloneNode(true);
                while (e_copy.firstChild != e_copy.lastChild) {
                    e_copy.removeChild(e_copy.lastChild);
                }
                if (e_copy.innerText.includes(innerStr)) {
                    res.push(e);
                }
            });
            return res;
        }

        function initUI() {
            // 页面未加载
            let uiAnchor = getElementsByInnerText('i', uiAnchorStr);
            if (!uiAnchor || !uiAnchor[0]) {
                return;
            }
            // 已初始化
            if (document.getElementById('danmakuCtr')) {
                return;
            }
            console.log('正在初始化UI');
            // 弹幕按钮容器div
            let parent = uiAnchor[0].parentNode.parentNode.parentNode;
            let menubar = document.createElement('div');
            menubar.id = 'danmakuCtr';
            if (!window.ede.episode_info) {
                menubar.style.opacity = 0.5;
            }
            parent.append(menubar);
            // 弹幕开关
            displayButtonOpts.innerText = danmaku_icons[window.ede.danmakuSwitch];
            menubar.appendChild(createButton(displayButtonOpts));
            // 手动匹配
            menubar.appendChild(createButton(searchButtonOpts));
            // 简繁转换
            // translateButtonOpts.title = chConverTtitle[window.ede.chConvert];
            // menubar.appendChild(createButton(translateButtonOpts));
            // 屏蔽等级
            // filterButtonOpts.innerText = filter_icons[parseInt(window.localStorage.getItem('danmakuFilterLevel') ? window.localStorage.getItem('danmakuFilterLevel') : 0)];
            // menubar.appendChild(createButton(filterButtonOpts));
            // 弹幕信息
            menubar.appendChild(createButton(infoButtonOpts));
            // 弹幕大小调整
            menubar.appendChild(createResizeButton());
            console.log('UI初始化完成');
        }

        function sendNotification(title, msg) {
            const Notification = window.Notification || window.webkitNotifications;
            console.log(msg);
            if (Notification.permission === 'granted') {
                return new Notification(title, {
                    body: msg,
                });
            } else {
                Notification.requestPermission((permission) => {
                    if (permission === 'granted') {
                        return new Notification(title, {
                            body: msg,
                        });
                    }
                });
            }
        }

        function getEmbyItemInfo() {
            return window.require(['pluginManager']).then((items) => {
                if (items) {
                    for (let i = 0; i < items.length; i++) {
                        const item = items[i];
                        if (item.pluginsList) {
                            for (let j = 0; j < item.pluginsList.length; j++) {
                                const plugin = item.pluginsList[j];
                                if (plugin && plugin.id == 'htmlvideoplayer') {
                                    return plugin._currentPlayOptions ? plugin._currentPlayOptions.item : null;
                                }
                            }
                        }
                    }
                }
                return null;
            });
        }

        async function getEpisodeInfo(is_auto = true) {
            let item = await getEmbyItemInfo();
            if (!item) {
                return null;
            }
            let _id;
            let animeName;
            let anime_id = -1;
            let episode;
            if (item.Type == 'Episode') {
                _id = item.SeasonId;
                animeName = item.SeriesName;
                episode = item.IndexNumber;
                let session = item.ParentIndexNumber;
                if (session != 1) {
                    animeName += ' ' + session;
                }
            } else {
                _id = item.Id;
                animeName = item.Name;
                episode = 'movie';
            }
            let _id_key = '_anime_id_rel_' + _id;
            let _name_key = '_anime_name_rel_' + _id;
            let _episode_key = '_episode_id_rel_' + _id + '_' + episode;
            if (is_auto) {
                if (window.localStorage.getItem(_episode_key)) {
                    return JSON.parse(window.localStorage.getItem(_episode_key));
                }
            }
            if (window.localStorage.getItem(_id_key)) {
                anime_id = window.localStorage.getItem(_id_key);
            }
            if (window.localStorage.getItem(_name_key)) {
                animeName = window.localStorage.getItem(_name_key);
            }
            if (!is_auto) {
                animeName = prompt('确认动画名:', animeName);
                if (animeName == null) throw new Error('用户取消确认动画名操作');
            }

            let searchUrl = 'https://api.9-ch.com/cors/https://api.dandanplay.net/api/v2/search/episodes?anime=' + animeName + '&withRelated=true';
            if (is_auto) {
                searchUrl += '&episode=' + episode;
            }
            let animaInfo = await fetch(searchUrl, {
                method: 'GET',
                headers: {
                    'Accept-Encoding': 'gzip',
                    Accept: 'application/json',
                    'User-Agent': navigator.userAgent,
                },
            })
                .then((response) => response.json())
                .catch((error) => {
                    console.log('查询失败:', error);
                    return null;
                });
            console.log('查询成功');
            console.log(animaInfo);
            let selecAnime_id = 1;
            if (anime_id != -1) {
                for (let index = 0; index < animaInfo.animes.length; index++) {
                    if (animaInfo.animes[index].animeId == anime_id) {
                        selecAnime_id = index + 1;
                    }
                }
            }
            if (!is_auto) {
                let anime_lists_str = list2string(animaInfo);
                console.log(anime_lists_str);
                selecAnime_id = prompt('选择:\n' + anime_lists_str, selecAnime_id);
                if (selecAnime_id == null) throw new Error('用户取消选择集数操作');
                selecAnime_id = parseInt(selecAnime_id) - 1;
                window.localStorage.setItem(_id_key, animaInfo.animes[selecAnime_id].animeId);
                window.localStorage.setItem(_name_key, animaInfo.animes[selecAnime_id].animeTitle);
                let episode_lists_str = ep2string(animaInfo.animes[selecAnime_id].episodes);
                episode = prompt('确认集数:\n' + episode_lists_str, parseInt(episode));
                if (episode == null) throw new Error('用户取消确认集数操作');
                episode = parseInt(episode) - 1;
            } else {
                selecAnime_id = parseInt(selecAnime_id) - 1;
                episode = 0;
            }
            let episodeInfo = {
                episodeId: animaInfo.animes[selecAnime_id].episodes[episode].episodeId,
                animeTitle: animaInfo.animes[selecAnime_id].animeTitle,
                episodeTitle: animaInfo.animes[selecAnime_id].type == 'tvseries' ? animaInfo.animes[selecAnime_id].episodes[episode].episodeTitle : null,
            };
            window.localStorage.setItem(_episode_key, JSON.stringify(episodeInfo));
            return episodeInfo;
        }

        function getComments(episodeId) {
            let url = 'https://api.9-ch.com/cors/https://api.dandanplay.net/api/v2/comment/' + episodeId + '?withRelated=true&chConvert=' + window.ede.chConvert;
            return fetch(url, {
                method: 'GET',
                headers: {
                    'Accept-Encoding': 'gzip',
                    Accept: 'application/json',
                    'User-Agent': navigator.userAgent,
                },
            })
                .then((response) => response.json())
                .then((data) => {
                    console.log('弹幕下载成功: ' + data.comments.length);
                    return data.comments;
                })
                .catch((error) => {
                    console.log('获取弹幕失败:', error);
                    return null;
                });
        }

        async function createDanmaku(comments) {
            if (!comments) {
                return;
            }
            if (window.ede.danmaku != null) {
                window.ede.danmaku.clear();
                window.ede.danmaku.destroy();
                window.ede.danmaku = null;
            }
            let _comments = danmakuFilter(danmakuParser(comments));
            console.log('弹幕加载成功: ' + _comments.length);

            while (!document.querySelector(mediaContainerQueryStr)) {
                await new Promise((resolve) => setTimeout(resolve, 200));
            }

            let _container = document.querySelector(mediaContainerQueryStr);
            let _media = document.querySelector(mediaQueryStr);
            if (!_media) throw new Error('用户已退出视频播放');
            window.ede.danmaku = new Danmaku({
                container: _container,
                media: _media,
                comments: _comments,
                engine: 'canvas',
            });
            setInterval(()=>{
                window.ede.danmakuSwitch == 1 ? window.ede.danmaku.show() : window.ede.danmaku.hide();
            },1000);
            if (window.ede.ob) {
                window.ede.ob.disconnect();
            }
            window.ede.ob = new ResizeObserver(() => {
                if (window.ede.danmaku) {
                    console.log('Resizing');
                    window.ede.danmaku.resize();
                }
            });
            window.ede.ob.observe(_container);
        }

        function loadDanmaku(loadType = LOAD_TYPE.CHECK) {
            if (window.ede.loading) {
                console.log('正在重新加载');
                return;
            }
            window.ede.loading = true;
            getEpisodeInfo(loadType !== LOAD_TYPE.SEARCH)
                .then((info) => {
                    return new Promise((resolve, reject) => {
                        if (!info) {
                            if (loadType !== LOAD_TYPE.INIT) {
                                reject('播放器未完成加载');
                            } else {
                                reject(null);
                            }
                        }
                        if (
                            loadType !== LOAD_TYPE.SEARCH &&
                            loadType !== LOAD_TYPE.REFRESH &&
                            loadType !== LOAD_TYPE.RELOAD &&
                            window.ede.danmaku &&
                            window.ede.episode_info &&
                            window.ede.episode_info.episodeId == info.episodeId
                        ) {
                            reject('当前播放视频未变动');
                        } else {
                            window.ede.episode_info = info;
                            resolve(info.episodeId);
                        }
                    });
                })
                .then(
                    (episodeId) => {
                        if (episodeId) {
                            if (loadType === LOAD_TYPE.RELOAD && danmuCache[episodeId]) {
                                createDanmaku(danmuCache[episodeId])
                                    .then(() => {
                                        console.log('弹幕就位');
                                    })
                                    .catch((err) => {
                                        console.log(err);
                                    });
                            } else {
                                getComments(episodeId).then((comments) => {
                                    danmuCache[episodeId] = comments;
                                    createDanmaku(comments)
                                        .then(() => {
                                            console.log('弹幕就位');
                                        })
                                        .catch((err) => {
                                            console.log(err);
                                        });
                                });
                            }
                        }
                    },
                    (msg) => {
                        if (msg) {
                            console.log(msg);
                        }
                    },
                )
                .then(() => {
                    window.ede.loading = false;
                    if (document.getElementById('danmakuCtr').style.opacity != 1) {
                        document.getElementById('danmakuCtr').style.opacity = 1;
                    }
                })
                .catch((err) => {
                    console.log(err);
                });
        }

        function danmakuFilter(comments) {
            let level = parseInt(window.localStorage.getItem('danmakuFilterLevel') ? window.localStorage.getItem('danmakuFilterLevel') : 0);
            if (level == 0) {
                return comments;
            }
            let limit = 9 - level * 2;
            let vertical_limit = 6;
            let arr_comments = [];
            let vertical_comments = [];
            for (let index = 0; index < comments.length; index++) {
                let element = comments[index];
                let i = Math.ceil(element.time);
                let i_v = Math.ceil(element.time / 3);
                if (!arr_comments[i]) {
                    arr_comments[i] = [];
                }
                if (!vertical_comments[i_v]) {
                    vertical_comments[i_v] = [];
                }
                // TODO: 屏蔽过滤
                if (vertical_comments[i_v].length < vertical_limit) {
                    vertical_comments[i_v].push(element);
                } else {
                    element.mode = 'rtl';
                }
                if (arr_comments[i].length < limit) {
                    arr_comments[i].push(element);
                }
            }
            return arr_comments.flat();
        }

        function danmakuParser($obj) {
            //const $xml = new DOMParser().parseFromString(string, 'text/xml')
            return $obj
                .map(($comment) => {
                    const p = $comment.p;
                    //if (p === null || $comment.childNodes[0] === undefined) return null
                    const values = p.split(',');
                    const mode = { 6: 'ltr', 1: 'rtl', 5: 'top', 4: 'bottom' }[values[1]];
                    if (!mode) return null;
                    //const fontSize = Number(values[2]) || 25
                    // 弹幕大小
                    const fontSizeMagnification = parseFloat(localStorage.getItem('danmakuFontSizeMagnification')) || 1;
                    const fontSize = Math.round((window.screen.height > window.screen.width ? window.screen.width : window.screen.height / 1080) * 18 * fontSizeMagnification);
                    // 弹幕透明度
                    const fontOpacity = Math.round((parseFloat(localStorage.getItem('danmakuFontOpacity')) || 1.0) * 255).toString(16);
                    // 弹幕颜色+透明度
                    const color = `000000${Number(values[2]).toString(16)}${fontOpacity}`.slice(-8);
                    return {
                        text: $comment.m,
                        mode,
                        time: values[0] * 1,
                        style: {
                            fontSize: `${fontSize}px`,
                            color: `#${color}`,
                            textShadow:
                                color === '00000' ? '-1px -1px #fff, -1px 1px #fff, 1px -1px #fff, 1px 1px #fff' : '-1px -1px #000, -1px 1px #000, 1px -1px #000, 1px 1px #000',

                            font: `${fontSize}px sans-serif`,
                            fillStyle: `#${color}`,
                            strokeStyle: color === '000000' ? `#ffffff${fontOpacity}` : `#000000${fontOpacity}`,
                            lineWidth: 2.0,
                        },
                    };
                })
                .filter((x) => x);
        }

        function list2string($obj2) {
            const $animes = $obj2.animes;
            let anime_lists = $animes.map(($single_anime) => {
                return $single_anime.animeTitle + ' 类型:' + $single_anime.typeDescription;
            });
            let anime_lists_str = '1:' + anime_lists[0];
            for (let i = 1; i < anime_lists.length; i++) {
                anime_lists_str = anime_lists_str + '\n' + (i + 1).toString() + ':' + anime_lists[i];
            }
            return anime_lists_str;
        }

        function ep2string($obj3) {
            const $animes = $obj3;
            let anime_lists = $animes.map(($single_ep) => {
                return $single_ep.episodeTitle;
            });
            let ep_lists_str = '1:' + anime_lists[0];
            for (let i = 1; i < anime_lists.length; i++) {
                ep_lists_str = ep_lists_str + '\n' + (i + 1).toString() + ':' + anime_lists[i];
            }
            return ep_lists_str;
        }

        while (!window.require) {
            await new Promise((resolve) => setTimeout(resolve, 200));
        }
        if (!window.ede) {
            window.ede = new EDE();
            setInterval(() => {
                initUI();
            }, check_interval);
            while (!(await getEmbyItemInfo())) {
                await new Promise((resolve) => setTimeout(resolve, 200));
            }
            loadDanmaku(LOAD_TYPE.INIT);
            setInterval(() => {
                initListener();
            }, check_interval);
        }
    }
})();