您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
在B站视频内外挂本地视频,主要用于看录播时和主播同步看B站直播不能放出画面的视频
// ==UserScript== // @name B站录播同步视听 // @namespace simultaneous-hearing-and-sight // @version 0.0.3 // @author icinggslits // @description 在B站视频内外挂本地视频,主要用于看录播时和主播同步看B站直播不能放出画面的视频 // @license MIT // @icon https://www.bilibili.com/favicon.ico // @match *://www.bilibili.com/video/* // @grant GM_addStyle // ==/UserScript== /* * 该脚本由Vite构建,源码见 https://github.com/icinggslits/simultaneous-hearing-and-sight */ (e=>{if(typeof GM_addStyle=="function"){GM_addStyle(e);return}const n=document.createElement("style");n.textContent=e,document.head.append(n)})(" :root{--measuringBlockResizeSize: 15px }.__sign_SHAS_panel{position:absolute;display:flex;flex-direction:column;background-color:#fff;box-shadow:0 1px 1px #0000000f,0 2px 4px #0000000f,0 4px 4px #0000000f,0 8px 8px #0000000f,0 16px 16px #0000000f,0 -4px 16px #00000012;z-index:100000}.__sign_SHAS_panel.hide{display:none}.__sign_SHAS_panel .top,.__sign_SHAS_panel .bottom{display:flex;width:100%;flex-basis:24px;background-color:#dcdcdc}.__sign_SHAS_panel .top .left{position:relative;display:flex;flex-grow:1}.__sign_SHAS_panel .top .left .back{flex-basis:24px;position:relative;cursor:pointer;display:none}.__sign_SHAS_panel .top .left .back .back_line_1{position:absolute;width:35%;height:1px;background-color:gray;left:7px;top:15px;transform:rotate(45deg)}.__sign_SHAS_panel .top .left .back .back_line_2{position:absolute;width:35%;height:1px;background-color:gray;left:7px;top:9px;transform:rotate(135deg)}.__sign_SHAS_panel .top .right{position:relative;flex-basis:24px;cursor:pointer}.__sign_SHAS_panel .top .right .close_line_1{position:absolute;width:65%;height:1px;background-color:gray;left:3px;top:11px;transform:rotate(45deg)}.__sign_SHAS_panel .top .right .close_line_2{position:absolute;width:65%;height:1px;background-color:gray;left:3px;top:11px;transform:rotate(-45deg)}.__sign_SHAS_panel .main{width:100%;flex-grow:1;flex-direction:column;flex-basis:100%;overflow:auto}.__sign_SHAS_panel .main .listPage .create_new_line{margin-top:4px}.__sign_SHAS_panel .main .listPage .create_new_line .create_new_line_button{font-size:20px;cursor:pointer;margin-left:22px;margin-top:8px;transition:background-color .3s;padding:0 4px;border-radius:4px}.__sign_SHAS_panel .main .listPage .create_new_line .create_new_line_button:hover{background-color:#dcdcdc}.__sign_SHAS_panel .main .listPage .content{display:flex;flex-direction:column;margin-top:10px;-webkit-user-select:none;user-select:none}.__sign_SHAS_panel .main .page.hide{display:none}.__sign_SHAS_panel .main .content .content_line,.__sign_SHAS_panel .main .content .content_column_line{display:flex;padding:10px 10px 0 20px}.__sign_SHAS_panel .main .content .content_line>div,.__sign_SHAS_panel .main .content .content_column_line>div{display:flex;justify-content:center;font-size:16px}.__sign_SHAS_panel .main .content .content_line .content_line_name{position:relative;cursor:pointer;padding-right:6px}.__sign_SHAS_panel .main .content .content_line .content_line_videoEdit{color:#dcdcdc;transition:color .3s}.__sign_SHAS_panel .main .content .content_line .content_line_videoEdit.editable{cursor:pointer;color:#000}.__sign_SHAS_panel .main .content .content_line .content_line_videoEdit.editable:hover{color:#00bfff}.__sign_SHAS_panel .main .content .content_line .content_line_name,.__sign_SHAS_panel .main .content .content_column_line .content_column_name{flex-basis:40%}.__sign_SHAS_panel .main .content .content_line .content_line_videoInput{flex-basis:60%;display:block;text-overflow:ellipsis;white-space:nowrap;overflow:hidden;cursor:pointer;transition:all .3s;text-align:center}.__sign_SHAS_panel .main .content .content_column_line .content_column_videoInput{flex-basis:60%}.__sign_SHAS_panel .main .content .content_line .content_line_videoInput:hover{color:#00bfff}.__sign_SHAS_panel .main .content .content_line .content_line_videoEdit,.__sign_SHAS_panel .main .content .content_line .content_line_volume,.__sign_SHAS_panel .main .content .content_column_line .content_column_edit,.__sign_SHAS_panel .main .content .content_column_line .content_column_volume{flex-basis:80px}.__sign_SHAS_panel .main .content .content_column_line .content_column_delete,.__sign_SHAS_panel .main .content .content_line .content_line_delete{flex-basis:36px}.__sign_SHAS_panel .main .content .content_line .content_line_delete{transition:all .3s;cursor:pointer;text-align:center;border-radius:12px}.__sign_SHAS_panel .main .content .content_line .content_line_delete:hover{background-color:#dcdcdc}.__sign_SHAS_panel .user_input,.__sign_SHAS_panel .user_input:focus{position:absolute;left:0;top:-1px;width:100%;height:100%;font-size:16px;display:block;outline:0;border:0;text-align:center}.__sign_SHAS_panel .main .editPage{display:block;flex-direction:column}.__sign_SHAS_panel .main .editPage .editPage_title{padding:10px 0;font-size:18px;text-align:center}.__sign_SHAS_panel .main .editPage .editPage_column{display:flex;padding:4px 0}.__sign_SHAS_panel .main .editPage .editPage_column .editPage_column_descriptions{text-align:center;justify-content:center;flex-basis:50%;font-size:16px}.__sign_SHAS_panel .main .editPage .editPage_column .editPage_column_occupy{padding:0 6px;flex-basis:20px;margin-right:30px}.__sign_SHAS_panel .main .editPage .content{padding:10px 0}.__sign_SHAS_panel .main .editPage .editPage_tool{display:flex;padding:0 4px;height:24px}.__sign_SHAS_panel .main .editPage .editPage_newPoint{display:inline;padding:0 4px;margin:0 10px;cursor:pointer;transition:all .3s;border-radius:4px;line-height:24px}.__sign_SHAS_panel .main .editPage .editPage_newPoint:hover{background-color:#dcdcdc}.__sign_SHAS_panel .main .editPage .editPage_export{position:relative;top:1px;font-size:14px;color:gray;cursor:pointer;transition:all .3s;padding:0 4px}.__sign_SHAS_panel .main .editPage .editPage_export:hover{color:#87ceeb}.__sign_SHAS_panel .main .editPage .editPage_export:active{outline:gainsboro 1px solid}.__sign_SHAS_panel .main .editPage .editPage_bottom{position:absolute;bottom:0;left:0;width:100%;height:24px;display:flex}.__sign_SHAS_panel .main .editPage .editPage_bottom .editPage_bottom_externalVideoTime{text-align:center;flex-basis:120px;font-size:12px;color:gray;line-height:24px}.__sign_SHAS_measuringBlock{position:absolute;cursor:grab;display:flex;opacity:0;pointer-events:none;transition:opacity .3s}.__sign_SHAS_measuringBlock.edit{pointer-events:all}.__sign_SHAS_measuringBlock.edit video{outline:gainsboro 1px solid;opacity:.5}.__sign_SHAS_measuringBlock.show,.__sign_SHAS_measuringBlock.alwaysShow{opacity:1;z-index:1}.__sign_SHAS_measuringBlock.alwaysShow{z-index:2}.__sign_SHAS_measuringBlock video{position:absolute;width:100%;height:100%;pointer-events:none;outline:transparent 1px solid;transition:opacity .3s,outline-color .3s}.__sign_SHAS_measuringBlock.edit .__sign_SHAS_measuringBlock_resize{background-color:#0000001a;position:absolute;right:0;bottom:0;cursor:nwse-resize;border-top:gainsboro 1px solid;border-left:gainsboro 1px solid;width:var(--measuringBlockResizeSize);height:var(--measuringBlockResizeSize)}.__sign_SHAS_videoArea{position:absolute;pointer-events:none}.__sign_SHAS_timestampEditor{width:200px;display:flex;flex-grow:1;flex-basis:80px;margin-left:10px}.__sign_SHAS_timestampEditor_hours,.__sign_SHAS_timestampEditor_minutes,.__sign_SHAS_timestampEditor_seconds,.__sign_SHAS_timestampEditor_milliseconds{position:relative;flex-grow:1;font-size:16px;text-align:center}.__sign_SHAS_referencePointLine{display:flex}.__sign_SHAS_referencePointLine_timestampEditorOfExternalVideo,.__sign_SHAS_referencePointLine_timestampEditorOfOriginalVideo{justify-content:center;flex-basis:50%}.__sign_SHAS_referencePointLine_delete{text-align:center;padding:0 2px;flex-basis:20px;margin-right:30px;cursor:pointer;transition:all .3s;border-radius:6px;line-height:24px}.__sign_SHAS_referencePointLine_delete:hover{background-color:#dcdcdc}.bpx-player-video-area video.ban{pointer-events:none}.__sign_SHAS_replyListImport{cursor:pointer;text-decoration:underline;transition:color .3s}.__sign_SHAS_replyListImport:hover{color:gray}.__sign_SHAS_replyListImport:active{transition:color 0s;color:#8b0000} "); (function () { 'use strict'; var __accessCheck = (obj, member, msg) => { if (!member.has(obj)) throw TypeError("Cannot " + msg); }; var __privateGet = (obj, member, getter) => { __accessCheck(obj, member, "read from private field"); return getter ? getter.call(obj) : member.get(obj); }; var __privateAdd = (obj, member, value) => { if (member.has(obj)) throw TypeError("Cannot add the same private member more than once"); member instanceof WeakSet ? member.add(obj) : member.set(obj, value); }; var __privateSet = (obj, member, value, setter) => { __accessCheck(obj, member, "write to private field"); setter ? setter.call(obj, value) : member.set(obj, value); return value; }; var __privateMethod = (obj, member, method) => { __accessCheck(obj, member, "access private method"); return method; }; var _intervals, _cb, _timeoutID, _inProgress, _execute, execute_fn; const Config = { // 面板开关按键 code: "KeyP", // 是否需要按住Ctrl ctrlKey: false, // 是否需要按住Alt altKey: false, // 是否需要按住Shift shiftKey: true, // 测量方块边长 measuringBlockResizeSize: 15, // 默认音量 defaultVolume: 40 }; const Convert = { /** * 转换为数字 * @param value {any} * @param unable {number} * @return {number} */ toNumber(value, unable = 0) { let result = unable; switch (true) { case typeof value === "string": (() => { result = Number(value.replace(/[^\d.-]/ig, "")); })(); break; case typeof value === "number": (() => { result = value; })(); break; } if (isFinite(result)) { return result; } else { return unable; } }, /** * 是否是数字 * * 字符串'25' - true * * @param value {any} * @return {boolean} */ isNumber(value) { return !isNaN(parseFloat(value)) && isFinite(value); }, /** * 过滤虚值返回规定的值 * @param value * @param unreal * @return {*|null} */ toReal(value, unreal = null) { if (!Convert.isReal(value)) { return unreal; } else { return value; } }, /** * 是否是实在的值 * @param value {any} * @return {boolean} */ isReal(value) { return !(value === void 0 || value === null || Number.isNaN(value)); }, /** * 转为Array * @template T * @param value {T} * @param arrayInArray {boolean} 是否包含数组 * @return {T[]} */ toList(value, arrayInArray = false) { if (Array.isArray(value) && Array.isArray(value[0]) === arrayInArray) { return value; } else { return [value]; } }, /** * 限制number在lower<=number<=upper之间,返回这个数 * @param number {number} * @param lower {number} * @param upper {number} * @return {number} */ limits(number, lower, upper) { return this.lowerLimit(this.upperLimit(number, upper), lower); }, /** * 限制number在number<=upper之间,返回这个数 * @param number * @param upper * @return {number} */ upperLimit(number, upper) { if (number > upper) { return upper; } else { return number; } }, /** * 限制number在lower<=number之间,返回这个数 * @param number * @param lower * @return {number} */ lowerLimit(number, lower) { if (number < lower) { return lower; } else { return number; } }, /** * 是否是Error * @param object * @return {boolean} */ isError(object) { return Error.prototype.isPrototypeOf(object); } }; const Range = document.createRange(); const createNode = (fragment) => { const contextualFragment = Range.createContextualFragment(fragment.trim()); return contextualFragment.childNodes[0]; }; const mouseDrag = (() => { let press = false; let isTarget = false; let targetCb; let original_x = 0; let original_y = 0; let onceData; const targetDomList = []; document.addEventListener("pointerdown", (event) => { const { x, y } = event; press = true; original_x = x; original_y = y; for (const [el, cb, onceCb] of targetDomList) { if (document.elementFromPoint(x, y) === el) { isTarget = true; targetCb = cb; onceData = onceCb == null ? void 0 : onceCb(); event.preventDefault(); break; } } }); document.addEventListener("pointerup", (event) => { press = false; isTarget = false; }); document.addEventListener("pointermove", (event) => { if (press && isTarget) { const { x, y } = event; targetCb({ original_x, original_y, x, y, diff_x: x - original_x, diff_y: y - original_y, onceData }); } }); return (el, cb, onceCb) => { targetDomList.push([el, cb, onceCb]); }; })(); const barDrag = /* @__PURE__ */ (() => { return (bar, body, limitNode = null) => { let original_left = 0; let original_top = 0; document.addEventListener("pointerdown", (event) => { const { x, y } = event; if (document.elementFromPoint(x, y) === bar) { original_left = Convert.toNumber(body.style.left); original_top = Convert.toNumber(body.style.top); } }); mouseDrag(bar, (dragInfo) => { const { diff_x, diff_y } = dragInfo; body.style.left = `${original_left + diff_x}px`; body.style.top = `${original_top + diff_y}px`; }); }; })(); const userSelectFile = (suffix) => { return new Promise((resolve, reject) => { const inputFileNode = createNode(`<input type="file" accept="${suffix}" />`); inputFileNode.addEventListener("change", () => { resolve(inputFileNode.files[0]); inputFileNode.remove(); }); inputFileNode.click(); }); }; const parseTime = { /** * * @param {number} seconds * @return {{milliseconds: number, hours: number, seconds: number, minutes: number}} */ secondsToTime(seconds) { const milliseconds = Math.floor(seconds * 1e3); const date = new Date(milliseconds); const hours = date.getUTCHours(); const minutes = date.getUTCMinutes(); const remainderSeconds = date.getUTCSeconds(); const remainderMilliseconds = date.getUTCMilliseconds(); return { hours, minutes, seconds: remainderSeconds, milliseconds: remainderMilliseconds }; } }; class Trigger { /** * * @param {function} cb * @param {number} intervals */ constructor(cb, intervals) { __privateAdd(this, _execute); __privateAdd(this, _intervals, 10); /** @type function */ __privateAdd(this, _cb, void 0); __privateAdd(this, _timeoutID, void 0); __privateAdd(this, _inProgress, false); __privateSet(this, _intervals, intervals); __privateSet(this, _cb, cb); } stop() { if (__privateGet(this, _inProgress)) { __privateSet(this, _inProgress, false); clearTimeout(__privateGet(this, _timeoutID)); } } start() { if (!__privateGet(this, _inProgress)) { __privateSet(this, _inProgress, true); __privateGet(this, _cb).call(this); __privateMethod(this, _execute, execute_fn).call(this); } } } _intervals = new WeakMap(); _cb = new WeakMap(); _timeoutID = new WeakMap(); _inProgress = new WeakMap(); _execute = new WeakSet(); execute_fn = function() { __privateSet(this, _timeoutID, setTimeout(() => { __privateGet(this, _cb).call(this); __privateMethod(this, _execute, execute_fn).call(this); }, __privateGet(this, _intervals))); }; const triggerBuilder = (cb, intervals = 100) => new Trigger(cb, intervals); const findInsertIndex = (numberList, x) => { let low = 0; let high = numberList.length - 1; while (low <= high) { let mid = Math.floor((low + high) / 2); if (numberList[mid] === x) { return mid; } else if (numberList[mid] < x) { low = mid + 1; } else { high = mid - 1; } } return low; }; const debounceExecuteBuilder = (fn, milliseconds = 300) => { const timeoutIDAttr = Symbol(); const debounceFn = () => { clearTimeout(debounceFn[timeoutIDAttr]); debounceFn[timeoutIDAttr] = setTimeout(() => fn(), milliseconds); }; debounceFn[timeoutIDAttr] = -1; return debounceFn; }; const triggerSource = { PLAY: Symbol("play"), PAUSE: Symbol("pause"), SEEKING: Symbol("seeking"), TRIGGER: Symbol("trigger") }; const videoProportions = () => { const bilibiliPlayerVideo2 = document.querySelector(".bpx-player-video-area video"); if (bilibiliPlayerVideo2) { return { videoWidth: bilibiliPlayerVideo2.videoWidth, videoHeight: bilibiliPlayerVideo2.videoHeight }; } else { return { videoWidth: 1920, videoHeight: 1080 }; } }; const { area, videoArea } = (() => { const area2 = document.querySelector(".bpx-player-video-area"); const videoArea2 = createNode(`<div class="__sign_SHAS_videoArea"></div>`); area2.appendChild(videoArea2); const { width, height } = area2.getBoundingClientRect(); area2.dataset.lastWidth = width.toString(); area2.dataset.lastHeight = height.toString(); return { area: area2, videoArea: videoArea2 }; })(); const updateArea = () => { const { width: areaWidth, height: areaHeight } = area.getBoundingClientRect(); const { videoWidth, videoHeight } = videoProportions(); const { width, height } = area.getBoundingClientRect(); if (videoHeight * (areaWidth / areaHeight) >= videoWidth) { const videoAreaWidth = areaHeight * (videoWidth / videoHeight); videoArea.style.left = `${(areaWidth - videoAreaWidth) / 2}px`; videoArea.style.top = `0`; videoArea.style.width = `${videoAreaWidth}px`; videoArea.style.height = "100%"; } else { const videoAreaHeight = areaWidth * (videoHeight / videoWidth); videoArea.style.top = `${(areaHeight - videoAreaHeight) / 2}px`; videoArea.style.left = `0`; videoArea.style.width = "100%"; videoArea.style.height = `${videoAreaHeight}px`; } area.dataset.lastWidth = width.toString(); area.dataset.lastHeight = height.toString(); }; { const ob = new ResizeObserver(() => { updateArea(); }); ob.observe(area); } const bilibiliPlayerVideo = document.querySelector(".bpx-player-video-area video"); const syncVideo = (source) => { const currentTime = bilibiliPlayerVideo.currentTime; const awaitingPlaybackList = []; const videoRange = externalVideoHub$1.allVideoRange(); for (const { targetMeasuringBlock, keyframeList: keyframeList2 } of videoRange) { let latestStartTime = null; let targetOriginalTime = 0; let targetExternalVideoTime = 0; const maxEndTime = (() => { let max = Number.MIN_VALUE; keyframeList2.forEach(({ endTime }) => endTime > max ? max = endTime : null); return max; })(); for (const keyframe of keyframeList2) { const { startTime, endTime, originalTime, externalVideoTime } = keyframe; if (currentTime >= originalTime && currentTime < maxEndTime) { if (latestStartTime === null) { latestStartTime = originalTime; targetOriginalTime = originalTime; targetExternalVideoTime = externalVideoTime; } else { if (latestStartTime < originalTime) { latestStartTime = originalTime; targetOriginalTime = originalTime; targetExternalVideoTime = externalVideoTime; } } } } if (latestStartTime !== null) { awaitingPlaybackList.push({ measuringBlock: targetMeasuringBlock, currentTime: targetExternalVideoTime + currentTime - targetOriginalTime }); } } that: for (const oneOfMeasuringBlock of externalVideoHub$1.allMeasuringBlock()) { for (const { measuringBlock, currentTime: currentTime2 } of awaitingPlaybackList) { if (oneOfMeasuringBlock === measuringBlock) { MeasuringBlock$1.show(measuringBlock); MeasuringBlock$1.videoSetCurrentTime(measuringBlock, currentTime2); switch (source) { case triggerSource.PAUSE: { MeasuringBlock$1.videoPause(measuringBlock); } break; case triggerSource.PLAY: { MeasuringBlock$1.videoPlay(measuringBlock); } break; case triggerSource.SEEKING: { MeasuringBlock$1.videoPause(measuringBlock); bilibiliPlayerVideo.addEventListener("canplay", () => { MeasuringBlock$1.videoPlay(measuringBlock); }, { once: true }); } break; case triggerSource.TRIGGER: { MeasuringBlock$1.videoPlay(measuringBlock); } break; } continue that; } } MeasuringBlock$1.videoPause(oneOfMeasuringBlock); MeasuringBlock$1.hide(oneOfMeasuringBlock); } }; let keyframeList = []; let nextKeyframeIndex = 0; const triggerCallback = []; const trigger = triggerBuilder(() => { const currentTime = bilibiliPlayerVideo.currentTime; const nextKeyframe = keyframeList[nextKeyframeIndex]; if (currentTime >= nextKeyframe) { syncVideo(triggerSource.TRIGGER); nextKeyframeIndex++; } triggerCallback.forEach((cb) => cb()); }); const videoTrigger = (callback) => { triggerCallback.push(callback); }; const resetTriggerPointer = () => { const referencePointList = externalVideoHub$1.allReferencePoint(); const currentTime = bilibiliPlayerVideo.currentTime; { keyframeList = []; for (const { originalTime, measuringBlock, externalVideoTime } of referencePointList) { keyframeList.push(originalTime); } nextKeyframeIndex = findInsertIndex(keyframeList, currentTime); } }; bilibiliPlayerVideo.addEventListener("play", () => { resetTriggerPointer(); syncVideo(triggerSource.PLAY); trigger.start(); }); bilibiliPlayerVideo.addEventListener("pause", () => { resetTriggerPointer(); syncVideo(triggerSource.PAUSE); trigger.stop(); }); bilibiliPlayerVideo.addEventListener("seeking", () => { resetTriggerPointer(); syncVideo(triggerSource.SEEKING); }); bilibiliPlayerVideo.addEventListener("ratechange", (event) => { MeasuringBlock$1.setAllVideoPlaybackRate(bilibiliPlayerVideo.playbackRate); }); let lastVideoPaused = false; let isBan = false; const bilibiliPlayerVideoController = { // 暂停b站视频并禁用交互 pauseAndBan() { isBan = true; lastVideoPaused = bilibiliPlayerVideo.paused; bilibiliPlayerVideo.pause(); bilibiliPlayerVideo.classList.add("ban"); }, // 解除禁用 relieve() { if (isBan) { isBan = false; bilibiliPlayerVideo.classList.remove("ban"); if (lastVideoPaused) { bilibiliPlayerVideo.pause(); } else { bilibiliPlayerVideo.play().then(); } } }, syncVideo() { resetTriggerPointer(); syncVideo(); }, /** * * @param {function} callback */ videoTrigger(callback) { videoTrigger(callback); } }; const regularExpression = { // 原本打算使用方括号,但是b站评论会自动把半角方括号变全角方括号([] -> 【】),所以换成了半角花括号 // // 匹配主体的正则 // patternMain: /(\[1v(.*?)])|(\{1v(.*?)})/g, // // // 匹配描述的正则 // patternDescription: /(\[(.*?)]$)|(\{(.*?)}$)/, // // // 匹配包含可能存在的描述的正则 // patternCompletely: /((\[(.*?)])?\[1v(.*?)])|((\{(.*?)})?\[1v(.*?)})/g, // 匹配主体的正则 patternMain: /\{1v(.*?)}/g, // 匹配描述的正则 patternDescription: /\{(.*?)}$/, // 匹配包含可能存在的描述的正则 patternCompletely: /(\{(.*?)})?\{1v(.*?)}/g }; const searchDeserializingData = (text) => { const list = []; const patternMain = regularExpression.patternMain; const patternDescription = regularExpression.patternDescription; let lastSliceIndex = 0; for (const regExpMatchArray of text.matchAll(patternMain)) { const frontText = regExpMatchArray.input.slice(lastSliceIndex, regExpMatchArray.index); const deserializingData = regExpMatchArray[1]; lastSliceIndex = regExpMatchArray.index + regExpMatchArray[0].length; let description = null; const descriptionMatch = frontText.match(patternDescription); if (descriptionMatch) { description = descriptionMatch[1]; } list.push({ description, deserializingData }); } return list; }; const contentLineMappingConfig = /* @__PURE__ */ new Map(); const contentLineMappingMeasuringBlock = /* @__PURE__ */ new Map(); let referencePointOfCache = []; const contentLineMappingFile = /* @__PURE__ */ new Map(); const serializeV1 = (contentLine) => { const { referencePointList } = contentLineMappingConfig.get(contentLine); const measuringBlock = contentLineMappingMeasuringBlock.get(contentLine); const list = []; for (const { originalTime, externalVideoTime } of referencePointList) { list.push(`${originalTime}>${externalVideoTime}`); } const listString = (() => { if (list.length === 0) { return "null"; } else { return list.join(","); } })(); return `{${ListContentLine.getName(contentLine)}}{1v${MeasuringBlock$1.serialize(measuringBlock)};${listString}}`; }; const targetReferencePoint = (referencePointLine) => { const { panelNode: panel2, editingContentLine: editingContentLine2 } = Panel; const referencePointLineList = panel2.querySelectorAll(".__sign_SHAS_referencePointLine"); for (let i = 0; i < referencePointLineList.length; i++) { const referencePointLineElement = referencePointLineList[i]; if (referencePointLineElement === referencePointLine) { const externalVideo1 = contentLineMappingConfig.get(editingContentLine2); if (externalVideo1) { const { versions, referencePointList } = externalVideo1; return [referencePointList[i], i]; } return null; } } return null; }; const updateReferencePointOfCache = () => { referencePointOfCache = []; const panel2 = Panel.panelNode; const contentLineList = panel2.querySelectorAll(".content_line"); let i = 0; for (const { versions, referencePointList } of contentLineMappingConfig.values()) { for (const { originalTime, externalVideoTime } of referencePointList) { referencePointOfCache.push({ originalTime, externalVideoTime, measuringBlock: contentLineMappingMeasuringBlock.get(contentLineList[i]) }); } i++; } referencePointOfCache.sort(({ originalTime: a }, { originalTime: b }) => a - b); }; const externalVideoHub = { addByContentLine(contentLine, measuringBlock) { contentLineMappingConfig.set(contentLine, { versions: "1", referencePointList: [] }); contentLineMappingMeasuringBlock.set(contentLine, measuringBlock); updateReferencePointOfCache(); }, /** * * @param contentLine * @return {?any} */ getMeasuringBlock(contentLine) { return contentLineMappingMeasuringBlock.get(contentLine); }, /** * * @param contentLine * @param {referencePoint} referencePoint * @return {boolean} */ addReferencePoint(contentLine, referencePoint) { const referencePointList = contentLineMappingConfig.get(contentLine); if (referencePointList) { referencePointList.referencePointList.push(referencePoint); updateReferencePointOfCache(); return true; } return false; }, /** * * @param contentLine * @return {externalVideo1} */ getReferencePointAll(contentLine) { return contentLineMappingConfig.get(contentLine); }, setOriginalTimeOfReferencePoint(referencePointLine, seconds) { const ok = targetReferencePoint(referencePointLine); if (ok) { const [referencePoint] = ok; referencePoint.originalTime = seconds; updateReferencePointOfCache(); } }, setExternalVideoTimeBySeconds(referencePointLine, seconds) { const ok = targetReferencePoint(referencePointLine); if (ok) { const [referencePoint] = ok; referencePoint.externalVideoTime = seconds; updateReferencePointOfCache(); } }, deleteReferencePoint(referencePointLine) { const ok = targetReferencePoint(referencePointLine); if (ok) { const [referencePoint, i] = ok; const { versions, referencePointList } = contentLineMappingConfig.get(Panel.editingContentLine); referencePointList.splice(i, 1); } }, /** * * @param contentLine * @param {File} file */ setFile(contentLine, file) { contentLineMappingFile.set(contentLine, file); }, /** * * @param contentLine * @return {?File} */ getFile(contentLine) { return contentLineMappingFile.get(contentLine); }, /** * 返回全部的referencePoint数组,根据originalTime按从小到大排序 * @return {{originalTime: number, externalVideoTime: number, measuringBlock}[]} */ allReferencePoint() { return referencePointOfCache; }, /** * * @return {any[]} */ allMeasuringBlock() { return [...contentLineMappingMeasuringBlock.values()]; }, /** * * @return {any[]} */ allContentLine() { return [...contentLineMappingMeasuringBlock.keys()]; }, /** * * @return {{targetMeasuringBlock, keyframeList: {startTime: number, endTime: number, originalTime: number, externalVideoTime: number}[]}[]} */ allVideoRange() { const videoRange = []; for (const [contentLine, measuringBlock] of contentLineMappingMeasuringBlock) { const { versions, referencePointList } = contentLineMappingConfig.get(contentLine); const measuringBlock2 = contentLineMappingMeasuringBlock.get(contentLine); const { currentTime, duration } = MeasuringBlock$1.getVideoInfo(measuringBlock2); const keyframeList2 = []; if (referencePointList.length > 0) { for (const { externalVideoTime, originalTime } of referencePointList) { const startTime = originalTime - externalVideoTime; const endTime = startTime + duration; keyframeList2.push({ startTime, endTime, originalTime, externalVideoTime }); } } videoRange.push({ targetMeasuringBlock: measuringBlock2, keyframeList: keyframeList2 }); } videoRange.sort(({ startTime: a }, { startTime: b }) => a - b); return videoRange; }, deleteContentLine(contentLine) { contentLineMappingConfig.delete(contentLine); contentLineMappingMeasuringBlock.delete(contentLine); contentLineMappingFile.delete(contentLine); }, /** * * @param contentLine * @return {?string} */ serialize(contentLine) { const { versions, referencePointList } = contentLineMappingConfig.get(contentLine); contentLineMappingMeasuringBlock.get(contentLine); return (() => { switch (versions) { case "1": { return serializeV1(contentLine); } } return null; })(); }, deserializeTo(contentLine) { } }; const externalVideoHub$1 = externalVideoHub; const videoAttr = Symbol(); const targetNodeAttr = Symbol(); const importedCreateAttr = Symbol("Marked as imported to create"); const measuringBlockMappingResize = /* @__PURE__ */ new Map(); const measuringBlockMappingPosition = /* @__PURE__ */ new Map(); const updateVideoRatios = (measuringBlock) => { const video = measuringBlock.querySelector("video"); measuringBlock.dataset.videoWidth = video.videoWidth; measuringBlock.dataset.videoHeight = video.videoHeight; updateArea(); }; const MeasuringBlock = { /** * 是否是MeasuringBlock对象 * @param measuringBlock * @return {boolean} */ is(measuringBlock) { var _a; return !!((_a = measuringBlock == null ? void 0 : measuringBlock.classList) == null ? void 0 : _a.contains(".__sign_SHAS_measuringBlock")); }, /** * * @param targetNode - 插入到targetNode */ create(targetNode) { const measuringBlock = createNode(` <div class="__sign_SHAS_measuringBlock"> <video></video> <div class="__sign_SHAS_measuringBlock_resize"></div> </div> `); measuringBlock.style.width = "300px"; measuringBlock.style.height = "200px"; measuringBlock.style.top = "0"; measuringBlock.style.left = "0"; const video = measuringBlock.querySelector("video"); measuringBlock[videoAttr] = video; measuringBlock[targetNodeAttr] = targetNode; video.addEventListener("loadedmetadata", () => { MeasuringBlock.setVideoRatios(measuringBlock, video.videoWidth, video.videoHeight); if (measuringBlock[importedCreateAttr] !== true) { MeasuringBlock.setPositionByRatio(measuringBlock, [0, 1], [0, 1]); } }); video.addEventListener("ended", () => { MeasuringBlock.hide(measuringBlock); }); const resizeBlock = measuringBlock.querySelector(".__sign_SHAS_measuringBlock_resize"); mouseDrag(resizeBlock, ({ diff_x, diff_y, onceData: { width, height } }) => { const { left: measuringBlockLeft, top: measuringBlockTop } = measuringBlock.getBoundingClientRect(); const { right: targetNodeRight, bottom: targetNodeBottom, width: targetNodeWidth, height: targetNodeHeight } = targetNode.getBoundingClientRect(); const maxResizeWidth = targetNodeRight - measuringBlockLeft; const maxResizeHeight = targetNodeBottom - measuringBlockTop; const resizeWidth = Convert.limits( width + diff_x, Config.measuringBlockResizeSize, maxResizeWidth ); if (Convert.isNumber(measuringBlock.dataset.videoWidth)) { const aspectRatio = Convert.toNumber(measuringBlock.dataset.videoHeight) / Convert.toNumber(measuringBlock.dataset.videoWidth); const resizeHeight = Convert.limits( height + diff_x * aspectRatio, Config.measuringBlockResizeSize, maxResizeHeight ); if (resizeHeight !== maxResizeHeight && resizeWidth !== maxResizeWidth) { this.setVideoRatioWidthAndKeepAspectRatio(measuringBlock, [resizeWidth, targetNodeWidth]); } } else { const resizeHeight = Convert.upperLimit( Convert.lowerLimit(height + diff_y, Config.measuringBlockResizeSize), maxResizeHeight ); measuringBlock.style.width = `${resizeWidth / targetNodeWidth * 100}%`; measuringBlock.style.height = `${resizeHeight / targetNodeHeight * 100}%`; } }, () => { const { width, height } = measuringBlock.getBoundingClientRect(); return { width, height }; }); mouseDrag(measuringBlock, ({ diff_x, diff_y, onceData: { top: originalTop, left: originalLeft, width: originalWidth, height: originalHeight } }) => { measuringBlock.getBoundingClientRect(); const { width: targetNodeWidth, height: targetNodeHeight, left: targetNodeLeft, right: targetNodeRight, top: targetNodeTop, bottom: targetNodeBottom } = targetNode.getBoundingClientRect(); const leftOfPx = originalLeft + diff_x; const topOfPx = originalTop + diff_y; const left = Convert.limits(leftOfPx - targetNodeLeft, 0, targetNodeWidth - originalWidth); const top = Convert.limits(topOfPx - targetNodeTop, 0, targetNodeHeight - originalHeight); this.setPositionByRatio(measuringBlock, [left, targetNodeWidth], [top, targetNodeHeight]); }, () => { const { top, left, width, height } = measuringBlock.getBoundingClientRect(); return { top, left, width, height }; }); targetNode.appendChild(measuringBlock); return measuringBlock; }, /** * * @param measuringBlock * @param {[number, number]} ratio */ setVideoRatioWidthAndKeepAspectRatio(measuringBlock, ratio) { const videoWidth = Convert.toNumber(measuringBlock.dataset.videoWidth); const videoHeight = Convert.toNumber(measuringBlock.dataset.videoHeight); const [width, parentWidth] = ratio; const aspectRatio = videoHeight / videoWidth; const { height: parentHeight } = measuringBlock[targetNodeAttr].getBoundingClientRect(); measuringBlock.style.width = `${width / parentWidth * 100}%`; measuringBlock.style.height = `${width * aspectRatio / parentHeight * 100}%`; measuringBlockMappingResize.set(measuringBlock, { width, parentWidth }); }, /** * * @param measuringBlock * @param {[number, number]} ratioX * @param {[number, number]} ratioY */ setPositionByRatio(measuringBlock, ratioX, ratioY) { const [leftOfPx, targetNodeWidth] = ratioX; const [topOfPx, targetNodeHeight] = ratioY; const left = leftOfPx / targetNodeWidth; const top = topOfPx / targetNodeHeight; measuringBlock.style.left = `${left * 100}%`; measuringBlock.style.top = `${top * 100}%`; measuringBlockMappingPosition.set(measuringBlock, { left: leftOfPx, top: topOfPx, parentWidth: targetNodeWidth, parentHeight: targetNodeHeight }); }, /** * 设置对应的视频尺寸比例 * @param measuringBlock * @param {number} width * @param {number} height */ setVideoRatios(measuringBlock, width, height) { measuringBlock.dataset.videoWidth = width.toString(); measuringBlock.dataset.videoHeight = height.toString(); const { width: parentWidth } = measuringBlock[targetNodeAttr].getBoundingClientRect(); const { width: measuringBlockWidth } = measuringBlock.getBoundingClientRect(); this.setVideoRatioWidthAndKeepAspectRatio(measuringBlock, [measuringBlockWidth, parentWidth]); }, show(measuringBlock) { var _a; (_a = measuringBlock == null ? void 0 : measuringBlock.classList) == null ? void 0 : _a.add("show"); }, alwaysShow(measuringBlock) { var _a; (_a = measuringBlock == null ? void 0 : measuringBlock.classList) == null ? void 0 : _a.add("alwaysShow"); }, closeAlwaysShow(measuringBlock) { var _a; (_a = measuringBlock == null ? void 0 : measuringBlock.classList) == null ? void 0 : _a.remove("alwaysShow"); }, editMode(measuringBlock) { var _a; (_a = measuringBlock == null ? void 0 : measuringBlock.classList) == null ? void 0 : _a.add("edit"); }, exitEditMode(measuringBlock) { var _a; (_a = measuringBlock == null ? void 0 : measuringBlock.classList) == null ? void 0 : _a.remove("edit"); }, allExitEditMode() { for (const measuringBlock of document.querySelectorAll(".__sign_SHAS_measuringBlock.edit")) { this.exitEditMode(measuringBlock); this.closeAlwaysShow(measuringBlock); } }, hide(measuringBlock) { var _a, _b, _c; if (((_a = measuringBlock == null ? void 0 : measuringBlock.classList) == null ? void 0 : _a.contains("alwaysShow")) === false) { (_b = measuringBlock == null ? void 0 : measuringBlock.classList) == null ? void 0 : _b.remove("show"); (_c = measuringBlock == null ? void 0 : measuringBlock.classList) == null ? void 0 : _c.remove("alwaysShow"); } }, hideAll() { for (const measuringBlock of document.querySelectorAll(".__sign_SHAS_measuringBlock")) { this.hide(measuringBlock); } }, /** * * @param measuringBlock * @param {string} videoFileUrl */ importVideo(measuringBlock, videoFileUrl) { const video = measuringBlock[videoAttr]; video.src = videoFileUrl; updateVideoRatios(measuringBlock); }, /** * * @param measuringBlock * @return {{currentTime: number, duration: number}} */ getVideoInfo(measuringBlock) { const { currentTime, duration } = measuringBlock[videoAttr]; return { currentTime, duration }; }, videoPlay(measuringBlock) { measuringBlock[videoAttr].play().then(); }, videoPause(measuringBlock) { measuringBlock[videoAttr].pause(); }, videoSetCurrentTime(measuringBlock, currentTime) { measuringBlock[videoAttr].currentTime = currentTime; }, videoAllPause() { for (const measuringBlock of document.querySelectorAll(".__sign_SHAS_measuringBlock")) { this.videoPause(measuringBlock); } }, /** * * @param measuringBlock * @param {number} volume */ setVideoVolume(measuringBlock, volume) { measuringBlock[videoAttr].volume = volume; }, /** * * @param measuringBlock * @param {number} playbackRate */ setVideoPlaybackRate(measuringBlock, playbackRate) { measuringBlock[videoAttr].playbackRate = playbackRate; }, /** * * @param {number} playbackRate */ setAllVideoPlaybackRate(playbackRate) { externalVideoHub$1.allMeasuringBlock().forEach((measuringBlock) => this.setVideoPlaybackRate(measuringBlock, playbackRate)); }, remove(measuringBlock) { measuringBlockMappingResize.delete(measuringBlock); measuringBlockMappingPosition.delete(measuringBlock); measuringBlock.remove(); }, /** * * @param measuringBlock * @return {string} */ serialize(measuringBlock) { const { width, parentWidth: parentWidthOfResize } = measuringBlockMappingResize.get(measuringBlock); const { left, top, parentWidth: parentWidthOfPosition, parentHeight } = measuringBlockMappingPosition.get(measuringBlock); return `${width},${parentWidthOfResize};${left},${parentWidthOfPosition},${top},${parentHeight}`; }, markAsImportedCreate(measuringBlock) { measuringBlock[importedCreateAttr] = true; } }; const MeasuringBlock$1 = MeasuringBlock; const numberNodeAttr = Symbol(); const editingCallbackList = []; const NumberEditor = { is(numberEditor) { var _a; return !!((_a = numberEditor == null ? void 0 : numberEditor.classList) == null ? void 0 : _a.contains("content_line")); }, create(defaultNumber = 60) { const numberEditor = createNode(` <div class="__sign_SHAS_numberEditor"> <div class="__sign_SHAS_numberEditor_number"> ${defaultNumber} </div> </div> `); numberEditor[numberNodeAttr] = numberEditor.querySelector(".__sign_SHAS_numberEditor_number"); numberEditor.addEventListener("wheel", (event) => { const { deltaY, shiftKey } = event; const number = (() => { if (shiftKey) { return 1; } else { return 5; } })(); if (deltaY > 0) { this.add(numberEditor, -number); } else if (deltaY < 0) { this.add(numberEditor, number); } event.preventDefault(); }); return numberEditor; }, /** * * @param numberEditor * @param {number} number */ add(numberEditor, number) { const originalNumber = Convert.toNumber(numberEditor[numberNodeAttr].textContent); const currentNumber = Convert.limits(originalNumber + number, 0, 100); if (originalNumber !== currentNumber) { numberEditor[numberNodeAttr].textContent = currentNumber; editingCallbackList.forEach((cb) => cb({ originalNumber, currentNumber })); } }, /** * * @param numberEditor * @param {numberEditorCallback} cb */ editing(numberEditor, cb) { editingCallbackList.push(cb); }, /** * * @param numberEditor * @return {number} */ getNumber(numberEditor) { return Convert.toNumber(numberEditor[numberNodeAttr].textContent); } }; const videoInputCallbackAttr = Symbol(); const videoEditCallbackAttr = Symbol(); const videoDeleteCallbackAttr = Symbol(); const bindMeasuringBlockAttr = Symbol(); const ListContentLine = { /** * * @param contentLine * @return {boolean} */ is(contentLine) { var _a; return !!((_a = contentLine == null ? void 0 : contentLine.classList) == null ? void 0 : _a.contains("content_line")); }, /** * * @param measuringBlock - 绑定的measuringBlock */ create(measuringBlock) { const contentLine = createNode(` <div class="content_line"> <div class="content_line_name">视频${document.querySelectorAll(".content_line").length + 1}</div> <div class="content_line_videoInput">点击选择视频源</div> <div class="content_line_volume"></div> <div class="content_line_videoEdit">编辑</div> <div class="content_line_delete">✖</div> </div> `); contentLine[bindMeasuringBlockAttr] = measuringBlock; { const defaultVolume = Config.defaultVolume; const numberEditor = NumberEditor.create(defaultVolume); MeasuringBlock$1.setVideoVolume(measuringBlock, defaultVolume / 100); NumberEditor.editing(numberEditor, ({ currentNumber }) => { MeasuringBlock$1.setVideoVolume(measuringBlock, currentNumber / 100); }); contentLine.querySelector(".content_line_volume").appendChild(numberEditor); } { const videoInput = contentLine.querySelector(".content_line_videoInput"); videoInput.dataset.videoUrl = "null"; videoInput.addEventListener("click", () => { userSelectFile(".mp4, .mkv, .flv").then((file) => { var _a; URL.revokeObjectURL(videoInput.dataset.videoUrl); const videoFileUrl = URL.createObjectURL(file); videoInput.dataset.videoUrl = videoFileUrl; contentLine.querySelector(".content_line_videoEdit").classList.add("editable"); videoInput.textContent = file.name; (_a = contentLine == null ? void 0 : contentLine[videoInputCallbackAttr]) == null ? void 0 : _a.call(contentLine, { videoFileUrl, file }); }); }); } { const lineNameNode = contentLine.querySelector(".content_line_name"); lineNameNode.addEventListener("click", () => { const input = createNode(`<input class="user_input" type="text" autocomplete="off" />`); input.value = lineNameNode.textContent; lineNameNode.appendChild(input); input.focus(); input.addEventListener("blur", () => { if (input.value.trim().length > 0) { this.setName(contentLine, input.value); } input.remove(); }); }); } { const videoEdit = contentLine.querySelector(".content_line_videoEdit"); videoEdit.addEventListener("click", () => { var _a; if (videoEdit.classList.contains("editable")) { (_a = contentLine == null ? void 0 : contentLine[videoEditCallbackAttr]) == null ? void 0 : _a.call(contentLine); } }); } { const videoDelete = contentLine.querySelector(".content_line_delete"); videoDelete.addEventListener("click", () => { var _a; (_a = contentLine == null ? void 0 : contentLine[videoDeleteCallbackAttr]) == null ? void 0 : _a.call(contentLine); }); } return contentLine; }, /** * 获取描述 * @param contentLine * @return {string} */ getName(contentLine) { return contentLine.querySelector(".content_line_name").textContent; }, /** * * @param contentLine * @param {string} name */ setName(contentLine, name) { const lineNameNode = contentLine.querySelector(".content_line_name"); lineNameNode.textContent = name; }, /** * 获取视频文件url * @param contentLine * @return {?string} */ getVideoUrl(contentLine) { var _a, _b; const url = (_b = (_a = contentLine.querySelector(".content_line_videoInput")) == null ? void 0 : _a.dataset) == null ? void 0 : _b.videoUrl; if ((url == null ? void 0 : url.length) > 4) { return url; } return null; }, /** * * @param contentLine * @param {videoInputCallback} cb */ setVideoInputCallback(contentLine, cb) { contentLine[videoInputCallbackAttr] = cb; }, /** * * @param contentLine * @param {function} cb */ setVideoEditCallback(contentLine, cb) { contentLine[videoEditCallbackAttr] = cb; }, /** * * @param contentLine * @param {function} cb */ setVideoDeleteCallback(contentLine, cb) { contentLine[videoDeleteCallbackAttr] = cb; }, remove(contentLine) { MeasuringBlock$1.remove(contentLine[bindMeasuringBlockAttr]); contentLine.remove(); } }; const editingCb = Symbol(); const TimestampEditor = { create() { const timestampEditor = createNode(` <div class="__sign_SHAS_timestampEditor"> <div class="__sign_SHAS_timestampEditor_hours" data-hours="0">00</div> <div class="__sign_SHAS_timestampEditor_semicolon">:</div> <div class="__sign_SHAS_timestampEditor_minutes" data-minutes="0">00</div> <div class="__sign_SHAS_timestampEditor_semicolon">:</div> <div class="__sign_SHAS_timestampEditor_seconds" data-seconds="0">00</div> <div class="__sign_SHAS_timestampEditor_semicolon">,</div> <div class="__sign_SHAS_timestampEditor_milliseconds" data-milliseconds="0">000</div> </div> `); const syncVideo2 = debounceExecuteBuilder(() => { bilibiliPlayerVideoController.syncVideo(); }, 100); const editing = () => { var _a; (_a = timestampEditor == null ? void 0 : timestampEditor[editingCb]) == null ? void 0 : _a.call(timestampEditor); syncVideo2(); }; timestampEditor.querySelector(".__sign_SHAS_timestampEditor_hours").addEventListener("wheel", (event) => { const { deltaY, shiftKey } = event; const hours = (() => { if (shiftKey) { return 10; } else { return 1; } })(); if (deltaY > 0) { TimestampEditor.hours(timestampEditor, -hours); } else if (deltaY < 0) { TimestampEditor.hours(timestampEditor, hours); } event.preventDefault(); editing(); }); timestampEditor.querySelector(".__sign_SHAS_timestampEditor_minutes").addEventListener("wheel", (event) => { const { deltaY, shiftKey } = event; const minutes = (() => { if (shiftKey) { return 10; } else { return 1; } })(); if (deltaY > 0) { TimestampEditor.minutes(timestampEditor, -minutes); } else if (deltaY < 0) { TimestampEditor.minutes(timestampEditor, minutes); } event.preventDefault(); editing(); }); timestampEditor.querySelector(".__sign_SHAS_timestampEditor_seconds").addEventListener("wheel", (event) => { const { deltaY, shiftKey } = event; const seconds = (() => { if (shiftKey) { return 10; } else { return 1; } })(); if (deltaY > 0) { TimestampEditor.seconds(timestampEditor, -seconds); } else if (deltaY < 0) { TimestampEditor.seconds(timestampEditor, seconds); } event.preventDefault(); editing(); }); timestampEditor.querySelector(".__sign_SHAS_timestampEditor_milliseconds").addEventListener("wheel", (event) => { const { deltaY, shiftKey, altKey } = event; const milliseconds = (() => { if (shiftKey && altKey) { return 1; } else if (shiftKey) { return 10; } else { return 100; } })(); if (deltaY > 0) { TimestampEditor.milliseconds(timestampEditor, -milliseconds); } else if (deltaY < 0) { TimestampEditor.milliseconds(timestampEditor, milliseconds); } event.preventDefault(); editing(); }); return timestampEditor; }, /** * 获取时间 * @param timestampEditor * @return {{hours: number, minutes: number, seconds: number, milliseconds: number}} */ getTime(timestampEditor) { const hoursNode = timestampEditor.querySelector(".__sign_SHAS_timestampEditor_hours"); const minutesNode = timestampEditor.querySelector(".__sign_SHAS_timestampEditor_minutes"); const secondsNode = timestampEditor.querySelector(".__sign_SHAS_timestampEditor_seconds"); const millisecondsNode = timestampEditor.querySelector(".__sign_SHAS_timestampEditor_milliseconds"); const hours = Convert.toNumber(hoursNode.dataset.hours); const minutes = Convert.toNumber(minutesNode.dataset.minutes); const seconds = Convert.toNumber(secondsNode.dataset.seconds); const milliseconds = Convert.toNumber(millisecondsNode.dataset.milliseconds); return { hours, minutes, seconds, milliseconds }; }, /** * 获取表示总秒数的时间 * @param timestampEditor * @return {number} */ getTimeOfSeconds(timestampEditor) { const { hours, minutes, seconds, milliseconds } = this.getTime(timestampEditor); return hours * 3600 + minutes * 60 + seconds + milliseconds / 1e3; }, /** * * @param timestampEditor * @param {number} hours */ setHours(timestampEditor, hours) { const hoursNode = timestampEditor.querySelector(".__sign_SHAS_timestampEditor_hours"); const hoursString = hours.toString(); hoursNode.textContent = hoursString.padStart(2, "0"); hoursNode.dataset.hours = hoursString; }, /** * * @param timestampEditor * @param {number} minutes */ setMinutes(timestampEditor, minutes) { const minutesNode = timestampEditor.querySelector(".__sign_SHAS_timestampEditor_minutes"); const minutesString = minutes.toString(); minutesNode.textContent = minutesString.padStart(2, "0"); minutesNode.dataset.minutes = minutesString; }, /** * * @param timestampEditor * @param {number} seconds */ setSeconds(timestampEditor, seconds) { const secondsNode = timestampEditor.querySelector(".__sign_SHAS_timestampEditor_seconds"); const secondsString = seconds.toString(); secondsNode.textContent = secondsString.padStart(2, "0"); secondsNode.dataset.seconds = secondsString; }, /** * * @param timestampEditor * @param {number} milliseconds */ setMilliseconds(timestampEditor, milliseconds) { const millisecondsNode = timestampEditor.querySelector(".__sign_SHAS_timestampEditor_milliseconds"); const millisecondsString = milliseconds.toString(); millisecondsNode.textContent = millisecondsString.padStart(3, "0"); millisecondsNode.dataset.milliseconds = millisecondsString; }, /** * * @param timestampEditor * @param {number} seconds */ setTimeBySeconds(timestampEditor, seconds) { const { hours, minutes, seconds: remainderSeconds, milliseconds } = parseTime.secondsToTime(seconds); this.setHours(timestampEditor, hours); this.setMinutes(timestampEditor, minutes); this.setSeconds(timestampEditor, remainderSeconds); this.setMilliseconds(timestampEditor, milliseconds); }, hours(timestampEditor, hours) { const { hours: currentHours } = this.getTime(timestampEditor); this.setHours(timestampEditor, Convert.limits(currentHours + hours, 0, 99)); }, minutes(timestampEditor, minutes) { const { hours, minutes: currentMinutes } = this.getTime(timestampEditor); const totalMinutes = Convert.limits(hours * 60 + currentMinutes + minutes, 0, 5999); this.setHours(timestampEditor, Math.floor(totalMinutes / 60)); this.setMinutes(timestampEditor, totalMinutes % 60); }, seconds(timestampEditor, seconds) { const { hours, minutes, seconds: currentSeconds } = this.getTime(timestampEditor); const totalSeconds = Convert.limits(hours * 3600 + minutes * 60 + currentSeconds + seconds, 0, 359940); this.setHours(timestampEditor, Math.floor(totalSeconds / 3600)); this.setMinutes(timestampEditor, Math.floor(totalSeconds % 3600 / 60)); this.setSeconds(timestampEditor, totalSeconds % 60); }, milliseconds(timestampEditor, milliseconds) { const { hours, minutes, seconds, milliseconds: currentMilliseconds } = this.getTime(timestampEditor); const totalSeconds = Convert.limits(hours * 3600 + minutes * 60 + seconds + (currentMilliseconds + milliseconds) / 1e3, 0, 359940); this.setHours(timestampEditor, Math.floor(totalSeconds / 3600)); this.setMinutes(timestampEditor, Math.floor(totalSeconds % 3600 / 60)); this.setSeconds(timestampEditor, Math.floor(totalSeconds % 60)); if (seconds >= 1 || minutes >= 1 || hours >= 1) { this.setMilliseconds(timestampEditor, (currentMilliseconds + Math.ceil(Math.abs(milliseconds / 1e3)) * 1e3 + milliseconds) % 1e3); } else { if (currentMilliseconds + milliseconds < 0) { this.setMilliseconds(timestampEditor, 0); } else { this.setMilliseconds(timestampEditor, (currentMilliseconds + Math.ceil(Math.abs(milliseconds / 1e3)) * 1e3 + milliseconds) % 1e3); } } }, /** * * @param timestampEditor * @param {function} cb */ editing(timestampEditor, cb) { timestampEditor[editingCb] = cb; } }; const originalVideoAttr = Symbol(); const externalVideoAttr = Symbol(); const deleteCallbackAttr = Symbol(); const ReferencePointLine = { /** * 是否是ReferencePointLine对象 * @param referencePointLine * @return {boolean} */ is(referencePointLine) { var _a; return !!((_a = referencePointLine == null ? void 0 : referencePointLine.classList) == null ? void 0 : _a.contains(".__sign_SHAS_referencePointLine")); }, create() { const referencePointLine = createNode(` <div class="__sign_SHAS_referencePointLine"> <div class="__sign_SHAS_referencePointLine_timestampEditorOfOriginalVideo"></div> <div class="__sign_SHAS_referencePointLine_timestampEditorOfExternalVideo"></div> <div class="__sign_SHAS_referencePointLine_delete">✖</div> </div> `); const timestampEditorOfOriginalVideo = TimestampEditor.create(); referencePointLine[originalVideoAttr] = timestampEditorOfOriginalVideo; TimestampEditor.editing(timestampEditorOfOriginalVideo, () => { const timeOfSeconds = TimestampEditor.getTimeOfSeconds(timestampEditorOfOriginalVideo); externalVideoHub$1.setOriginalTimeOfReferencePoint(referencePointLine, timeOfSeconds); }); referencePointLine.querySelector(".__sign_SHAS_referencePointLine_timestampEditorOfOriginalVideo").appendChild(timestampEditorOfOriginalVideo); const timestampEditorOfExternalVideo = TimestampEditor.create(); referencePointLine[externalVideoAttr] = timestampEditorOfExternalVideo; TimestampEditor.editing(timestampEditorOfExternalVideo, () => { const timeOfSeconds = TimestampEditor.getTimeOfSeconds(timestampEditorOfExternalVideo); externalVideoHub$1.setExternalVideoTimeBySeconds(referencePointLine, timeOfSeconds); }); referencePointLine.querySelector(".__sign_SHAS_referencePointLine_timestampEditorOfExternalVideo").appendChild(timestampEditorOfExternalVideo); referencePointLine.querySelector(".__sign_SHAS_referencePointLine_delete").addEventListener("click", () => { var _a; externalVideoHub$1.deleteReferencePoint(referencePointLine); (_a = referencePointLine == null ? void 0 : referencePointLine[deleteCallbackAttr]) == null ? void 0 : _a.call(referencePointLine); }); return referencePointLine; }, setOriginalTimeBySeconds(referencePointLine, seconds) { const timestampEditorOfOriginalVideo = referencePointLine[originalVideoAttr]; TimestampEditor.setTimeBySeconds(timestampEditorOfOriginalVideo, seconds); }, setExternalVideoTimeBySeconds(referencePointLine, seconds) { const timestampEditorOfExternalVideo = referencePointLine[externalVideoAttr]; TimestampEditor.setTimeBySeconds(timestampEditorOfExternalVideo, seconds); }, setTimeBySeconds(referencePointLine, originalTime, externalVideoTime) { this.setOriginalTimeBySeconds(referencePointLine, originalTime); this.setExternalVideoTimeBySeconds(referencePointLine, externalVideoTime); }, /** * * @param referencePointLine * @param {function} cb */ setDeleteCallback(referencePointLine, cb) { referencePointLine[deleteCallbackAttr] = cb; } }; const panelConfig = { width: 500, height: 400 }; let isOpen = false; let firstOpen = true; const panel = createNode(` <div class="__sign_SHAS_panel"> <div class="top"> <div class="left"> <div class="back"> <div class="back_line_1"></div> <div class="back_line_2"></div> </div> </div> <div class="right"> <div class="close_line_1"></div> <div class="close_line_2"></div> </div> </div> <div class="main"> <div class="listPage page"> <div class="content"> <div class="content_column_line"> <div class="content_column_name">描述</div> <div class="content_column_videoInput">视频源</div> <div class="content_column_volume">音量</div> <div class="content_column_edit">轴对轴</div> <div class="content_column_delete"></div> </div> </div> <div class="create_new_line"> <span class="create_new_line_button">✚</span> </div> </div> <div class="editPage page hide"> <div class="editPage_title"></div> <div class="editPage_column"> <div class="editPage_column_descriptions">网站视频</div> <div class="editPage_column_descriptions">挂载视频</div> <div class="editPage_column_occupy"></div> </div> <div class="content"></div> <div class="editPage_tool"> <div class="editPage_newPoint">✚</div> <div class="editPage_export">复制配置</div> </div> <div class="editPage_bottom"> <div class="editPage_bottom_externalVideoTime"></div> </div> </div> </div> </div> `); panel.querySelector(".main"); let editingContentLine; const createLine = () => { const measuringBlock = MeasuringBlock$1.create(document.querySelector(".__sign_SHAS_videoArea")); const contentLine = ListContentLine.create(measuringBlock); ListContentLine.setVideoEditCallback(contentLine, () => { panelPage.toEdit(contentLine); }); ListContentLine.setVideoInputCallback(contentLine, ({ videoFileUrl, file }) => { MeasuringBlock$1.importVideo(measuringBlock, videoFileUrl); externalVideoHub$1.setFile(contentLine, file); }); ListContentLine.setVideoDeleteCallback(contentLine, () => { externalVideoHub$1.deleteContentLine(contentLine); ListContentLine.remove(contentLine); }); externalVideoHub$1.addByContentLine(contentLine, measuringBlock); const content = panel.querySelector(".listPage .content"); content.appendChild(contentLine); return contentLine; }; const createLineByDeserialize = (deserializingData, description = null) => { const contentLine = createLine(); const measuringBlock = externalVideoHub$1.getMeasuringBlock(contentLine); const { width, parentWidthOfResize, left, parentWidthOfPosition, top, parentHeight, referencePointList } = deserializeV1(deserializingData); MeasuringBlock$1.markAsImportedCreate(measuringBlock); MeasuringBlock$1.setVideoRatioWidthAndKeepAspectRatio(measuringBlock, [width, parentWidthOfResize]); MeasuringBlock$1.setPositionByRatio(measuringBlock, [left, parentWidthOfPosition], [top, parentHeight]); referencePointList.forEach((referencePoint) => externalVideoHub$1.addReferencePoint(contentLine, referencePoint)); if (description) { ListContentLine.setName(contentLine, description); } }; const deserializeV1 = (deserializingData) => { const infoBlockList = deserializingData.split(";"); const resizeInfo = infoBlockList[0]; const resizeList = resizeInfo.split(","); const width = Convert.toNumber(resizeList[0]); const parentWidthOfResize = Convert.toNumber(resizeList[1]); const positionInfo = infoBlockList[1]; const positionList = positionInfo.split(","); const left = Convert.toNumber(positionList[0]); const parentWidthOfPosition = Convert.toNumber(positionList[1]); const top = Convert.toNumber(positionList[2]); const parentHeight = Convert.toNumber(positionList[3]); const referencePointListInfo = infoBlockList[2]; const referencePointList = []; for (const referencePointString of referencePointListInfo.split(",")) { const list = referencePointString.split(">"); const originalTime = Convert.toNumber(list[0]); const externalVideoTime = Convert.toNumber(list[1]); referencePointList.push({ originalTime, externalVideoTime }); } return { width, parentWidthOfResize, left, parentWidthOfPosition, top, parentHeight, referencePointList }; }; const createReferencePointLineNodeOnly = (originalTime, externalVideoTime) => { const referencePointLine = ReferencePointLine.create(); ReferencePointLine.setTimeBySeconds(referencePointLine, originalTime, externalVideoTime); ReferencePointLine.setDeleteCallback(referencePointLine, () => { buildEditPage(editingContentLine); }); panel.querySelector(".__sign_SHAS_panel .main .editPage .content").appendChild(referencePointLine); return referencePointLine; }; const buildEditPage = (contentLine) => { clearReferencePointLineNode(); const { versions, referencePointList } = externalVideoHub$1.getReferencePointAll(contentLine); for (const { originalTime, externalVideoTime } of referencePointList) { createReferencePointLineNodeOnly(originalTime, externalVideoTime); } }; const clearReferencePointLineNode = () => { [...panel.querySelectorAll(".__sign_SHAS_referencePointLine")].forEach((node) => node.remove()); }; const pageType = { list: Symbol("list"), edit: Symbol("edit") }; const panelPage = (() => { let currentPage = pageType.list; const hideAllPage = () => { for (const page of panel.querySelectorAll(".page")) { page.classList.add("hide"); } }; const backButton = { hide() { panel.querySelector(".back").style.display = "none"; }, display() { panel.querySelector(".back").style.display = "block"; } }; return { // 切换到列表页 toList() { currentPage = pageType.list; hideAllPage(); panel.querySelector(".listPage").classList.remove("hide"); editingContentLine = null; backButton.hide(); MeasuringBlock$1.allExitEditMode(); }, // 切换到编辑页 toEdit(contentLine) { currentPage = pageType.edit; hideAllPage(); editingContentLine = contentLine; const name = ListContentLine.getName(contentLine); { buildEditPage(contentLine); } { const measuringBlock = externalVideoHub$1.getMeasuringBlock(contentLine); if (measuringBlock) { MeasuringBlock$1.alwaysShow(measuringBlock); MeasuringBlock$1.editMode(measuringBlock); } } panel.querySelector(".editPage").classList.remove("hide"); panel.querySelector(".editPage_title").textContent = name; backButton.display(); }, get currentPage() { return currentPage; } }; })(); const initPanel = () => { { panel.style.left = `${(window.innerWidth - panelConfig.width) / 2}px`; panel.style.top = "100px"; panel.style.width = `${panelConfig.width}px`; panel.style.height = `${panelConfig.height}px`; } document.body.appendChild(panel); { const showExternalVideoTimeNode = panel.querySelector(".editPage_bottom_externalVideoTime"); bilibiliPlayerVideoController.videoTrigger(() => { if (panelPage.currentPage === pageType.edit) { const { currentTime, duration } = MeasuringBlock$1.getVideoInfo(externalVideoHub$1.getMeasuringBlock(editingContentLine)); const { milliseconds, seconds, minutes, hours } = parseTime.secondsToTime(currentTime); showExternalVideoTimeNode.textContent = `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")},${milliseconds.toString().padStart(3, "0")}`; } }); } { const dragRegion = panel.querySelector(".top .left"); barDrag(dragRegion, panel); } panel.querySelector(".top .right").addEventListener("click", Panel.close); { panel.querySelector(".create_new_line_button").addEventListener("click", () => { createLine(); }); panel.querySelector(".editPage_newPoint").addEventListener("click", () => { const { currentTime } = MeasuringBlock$1.getVideoInfo(externalVideoHub$1.getMeasuringBlock(editingContentLine)); const originalTime = Convert.toNumber(bilibiliPlayerVideo.currentTime.toFixed(3)); const externalVideoTime = Convert.toNumber(currentTime.toFixed(3)); createReferencePointLineNodeOnly(originalTime, externalVideoTime); externalVideoHub$1.addReferencePoint(editingContentLine, { originalTime, externalVideoTime }); bilibiliPlayerVideoController.syncVideo(); }); panel.querySelector(".back").addEventListener("click", () => { panelPage.toList(); bilibiliPlayerVideoController.syncVideo(); }); panel.querySelector(".editPage_export").addEventListener("click", () => { const configText = externalVideoHub$1.serialize(editingContentLine); if (configText) { const list = externalVideoHub$1.getReferencePointAll(editingContentLine); if (list.referencePointList.length > 0) { navigator.clipboard.writeText(configText).then(); } } }); } { window.addEventListener("paste", (event) => { const { clipboardData } = event; if (panelPage.currentPage === pageType.list && isOpen) { const text = clipboardData.getData("text/plain"); searchDeserializingData(text).forEach(({ deserializingData, description }) => createLineByDeserialize(deserializingData, description)); } }); } }; const Panel = { /** * @return {boolean} */ get initialised() { return !firstOpen; }, get panelNode() { return panel; }, get editingContentLine() { return editingContentLine; }, open() { isOpen = true; if (firstOpen) { firstOpen = false; initPanel(); } panel.classList.remove("hide"); }, close() { panelPage.toList(); isOpen = false; panel.classList.add("hide"); }, toggle() { if (isOpen) { this.close(); } else { this.open(); } }, toList() { panelPage.toList(); }, /** * 导入配置字符串创建挂载视频 * @param {string} deserializingData * @param {?string} description */ createLineByDeserialize(deserializingData, description = null) { createLineByDeserialize(deserializingData, description); } }; const replyList = document.querySelector(".left-container-under-player"); if (replyList) { const options = { childList: true, subtree: true }; const replyContentNode = /* @__PURE__ */ new Set(); const ob = new MutationObserver((mutations, observer) => { const nodeList = []; for (const mutation of mutations) { const { target } = mutation; if (target.classList.contains("reply-content") && !replyContentNode.has(target)) { replyContentNode.add(target); nodeList.push(target); } } observer.disconnect(); nodeList.forEach((node) => { const pattern = regularExpression.patternCompletely; node.innerHTML = node.innerHTML.replaceAll(pattern, (text) => `<span class="__sign_SHAS_replyListImport">${text}</span>`); for (const clickNode of node.querySelectorAll(".__sign_SHAS_replyListImport")) { clickNode.addEventListener("click", () => { const data = searchDeserializingData(clickNode.textContent); if (data.length > 0) { const [{ deserializingData, description }] = data; Panel.createLineByDeserialize(deserializingData, description); Panel.open(); Panel.toList(); } }); } }); observer.observe(replyList, options); }); ob.observe(replyList, options); } window.addEventListener("keydown", (event) => { const { code, ctrlKey, shiftKey, altKey } = event; if (code === Config.code && ctrlKey === Config.ctrlKey && shiftKey === Config.shiftKey && altKey === Config.altKey) { const focusNode = document.body.querySelector("*:focus"); if (!focusNode) { Panel.toggle(); } } }); })();