// ==UserScript== // @name 外挂弹幕插件 // @version 0.1.1 // @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 // @icon https://avatars.githubusercontent.com/u/1879224?v=4 // @require https://cdn.jsdelivr.net/npm/@xpadev-net/niconicomments@0.2.55/dist/bundle.min.js // @require https://cdn.jsdelivr.net/npm/danmaku@2.0.6/dist/danmaku.min.js // @namespace https://greasyfork.org/users/927887 // @downloadURL none // ==/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 = `
` 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 = `
` 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(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>') .replace(/'/g, "'") .replace(/"/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>/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: { fontSize: danmu.fontsize + '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 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); })(); (function createToolbar(config) { let html = `
` function getToolbarSetting() { if (localStorage['dfToolbar']) { return JSON.parse(localStorage['dfToolbar']) } else { return {} } } function saveToolbarSetting(setting) { localStorage['dfToolbar'] = JSON.stringify(setting) } 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 = getToolbarSetting() 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) } expandToolbar() setTimeout(collapseToolbar, 3000) 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; let currentSetting = getToolbarSetting() currentSetting.offsetTopPercent = toolbar.offsetTop / window.innerHeight saveToolbarSetting(currentSetting) } } window.addEventListener('mousemove', draggingHandle); window.addEventListener('mouseup', dragEndHandle); }) ({ options: { "加载本地弹幕": function createFileSelector() { const input = document.createElement('input'); input.type = 'file'; 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 buildContainer(videoElem) { // Get a reference to the existing element in the document let html = `
` videoElem.parentElement.insertAdjacentHTML('beforeend', html); return videoElem.parentElement.querySelector("#danmaku-container") } let videoElem 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 = new Danmaku({ container: buildContainer(videoElem), media: videoElem, comments: [] }); 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() } })()