// ==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/[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(/&/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 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: {
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 = `
<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>
`
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 = `
<style>
#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;
}
</style>
<div id="danmaku-container">
</div>
`
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()
}
})()