diff --git a/components/ScList/index.tsx b/components/ScList/index.tsx index 9027274..2af0f3f 100644 --- a/components/ScList/index.tsx +++ b/components/ScList/index.tsx @@ -1,115 +1,163 @@ -import { createRef, useCallback, useEffect, useRef, useState } from 'react' -import { - CSSTransition, - TransitionGroup, -} from 'react-transition-group' -import { useMove } from './hook' -import { eventBus } from '@/utils/event' -import { WS_SC_EVENT } from '@/constant' -import closeIcon from '~/assets/close.svg'; -import './index.less' +import { createRef, useCallback, useEffect, useRef, useState } from "react"; +import { CSSTransition, TransitionGroup } from "react-transition-group"; +import { useMove } from "./hook"; +import { eventBus } from "@/utils/event"; +import { WS_SC_EVENT } from "@/constant"; +import closeIcon from "~/assets/close.svg"; +import "./index.less"; interface ScInfo { - face: string - face_frame: string - uname: string - name_color: string - price: number - message: string - message_font_color: string - background_bottom_color: string - background_color: string - id: number - time: number - nodeRef: React.RefObject + face: string; + face_frame: string; + uname: string; + name_color: string; + price: number; + message: string; + message_font_color: string; + background_bottom_color: string; + background_color: string; + id: number; + time: number; + nodeRef: React.RefObject; + delay: number; } interface SCListProps { - scDocument: Document + scDocument: Document; } function SCList(props: SCListProps) { - const { scDocument } = props - const [scList, setScList] = useState([]) - const scListRef = useRef(null) - const { left, bottom, maxHeight } = useMove(scListRef, scDocument) - const timeoutMap = useRef(new Map()) + const { scDocument } = props; + const [scList, setScList] = useState([]); + const scListRef = useRef(null); + const { left, bottom, maxHeight } = useMove(scListRef, scDocument); + const timeoutMap = useRef(new Map()); - const Listener = useCallback((scInfo: ScInfo) => { - const existDeleteIdList = JSON.parse(sessionStorage.getItem('deleteId') ?? 'null') - if (Array.isArray(existDeleteIdList) && existDeleteIdList.indexOf(scInfo.id) > -1) { - console.log(`该id已被删除`) - return - } - setScList(prev => prev.concat({ ...scInfo, nodeRef: createRef() })) - const { id, time } = scInfo - if (!timeoutMap.current.has(id)) { - const timeout = setTimeout(() => { - setScList(prev => prev.filter(item => item.id !== id)) - }, time * 1000) + const Listener = useCallback((scInfo: ScInfo) => { + const existDeleteIdList = JSON.parse(sessionStorage.getItem("deleteId") ?? "null"); + if (Array.isArray(existDeleteIdList) && existDeleteIdList.indexOf(scInfo.id) > -1) { + console.log(`该id已被删除`); + return; + } + setScList((prev) => prev.concat({ ...scInfo, nodeRef: createRef() })); + const { id, time } = scInfo; + if (!timeoutMap.current.has(id)) { + const timeout = setTimeout(() => { + setScList((prev) => prev.filter((item) => item.id !== id)); + }, time * 1000); - timeoutMap.current.set(id, timeout) - } - }, []) + timeoutMap.current.set(id, timeout); + } + }, []); - useEffect(() => { - eventBus.subscribe(WS_SC_EVENT, Listener) + useEffect(() => { + eventBus.subscribe(WS_SC_EVENT, Listener); - let timeoutMapRefValue: Map | null = null + let timeoutMapRefValue: Map | null = null; - if (timeoutMap.current) - timeoutMapRefValue = timeoutMap.current + if (timeoutMap.current) timeoutMapRefValue = timeoutMap.current; - return () => { - eventBus.unsubscribe(WS_SC_EVENT, Listener) - if (timeoutMapRefValue?.size && timeoutMapRefValue.size > 0) { - for (const [_, value] of timeoutMapRefValue.entries()) - clearTimeout(value) - } - } - }, [Listener]) + return () => { + eventBus.unsubscribe(WS_SC_EVENT, Listener); + if (timeoutMapRefValue?.size && timeoutMapRefValue.size > 0) { + for (const [_, value] of timeoutMapRefValue.entries()) clearTimeout(value); + } + }; + }, [Listener]); - const handleDelete = (e: React.MouseEvent, id: number) => { - e.stopPropagation() - setScList(prev => prev.filter(item => item.id !== id)) - const existDeleteIdList = JSON.parse(sessionStorage.getItem('deleteId') ?? 'null') - sessionStorage.setItem('deleteId', JSON.stringify(Array.isArray(existDeleteIdList) ? existDeleteIdList.concat([id]) : [id])) - } + const handleDelete = (e: React.MouseEvent, id: number) => { + e.stopPropagation(); + setScList((prev) => prev.filter((item) => item.id !== id)); + const existDeleteIdList = JSON.parse(sessionStorage.getItem("deleteId") ?? "null"); + sessionStorage.setItem("deleteId", JSON.stringify(Array.isArray(existDeleteIdList) ? existDeleteIdList.concat([id]) : [id])); + }; - return ( -
- - {scList.map(({ face, face_frame, uname, name_color, price, message, message_font_color, background_bottom_color, background_color, id, nodeRef, time }) => - ( - -
}> -
-
}> -
-
-
-
-
- {uname} - - {price} - 元 - -
- 关闭 handleDelete(e, id)} /> -
-
{message}
-
-
-
-
-
- - ), - )} - -
- ) + return ( +
+ + {scList.map( + ({ + face, + face_frame, + uname, + name_color, + price, + message, + message_font_color, + background_bottom_color, + background_color, + id, + nodeRef, + time, + delay, + }) => ( + +
}> +
+
} + > +
+
+
+
+
+ + {uname} + + {price}元 +
+ 关闭 handleDelete(e, id)} /> +
+
+ {message} +
+
+
+
+
+
+ + ) + )} + +
+ ); } -export default SCList +export default SCList; diff --git a/dev/testData.js b/dev/testData.js index d20a634..7030b2b 100644 --- a/dev/testData.js +++ b/dev/testData.js @@ -46,7 +46,8 @@ export const testData = { price: 30, rate: 1000, start_time: 1720369969, - time: 600, + time: 60, + delay: 10, token: '43C86633', trans_mark: 0, ts: 1720369969, diff --git a/entrypoints/content.tsx b/entrypoints/content.tsx index aafbbc8..d74e246 100644 --- a/entrypoints/content.tsx +++ b/entrypoints/content.tsx @@ -1,52 +1,49 @@ +import type { Root } from "react-dom/client"; +import { createRoot } from "react-dom/client"; +import { LiveWS } from "bilibili-live-ws"; +import SCList from "../components/ScList"; -import type { Root } from 'react-dom/client' -import { createRoot } from 'react-dom/client' -import { LiveWS } from 'bilibili-live-ws' -import SCList from '../components/ScList' - -// import { testData } from '../dev/testData' -import { processData } from '@/utils' -import type { DanmuDataProps } from '@/utils' +// import { testData } from "../dev/testData"; +import { processData } from "@/utils"; +import type { DanmuDataProps } from "@/utils"; export default defineContentScript({ - matches: ['https://live.bilibili.com/*'], + matches: ["https://live.bilibili.com/*"], main() { - let existElement: HTMLElement | null - let root: Root | null - let liveWS: LiveWS | null - let resizeObserver: ResizeObserver | null - let switchState: boolean = true - let isMount = false - let isFirst = true - let isInIframe = false - + let existElement: HTMLElement | null; + let root: Root | null; + let liveWS: LiveWS | null; + let resizeObserver: ResizeObserver | null; + let switchState: boolean = true; + let isMount = false; + let isFirst = true; + let isInIframe = false; // 判断全屏模式 document.addEventListener( - 'fullscreenchange', + "fullscreenchange", () => { // 当用户在popup关闭此功能后 - if (!switchState) - return + if (!switchState) return; // 从全屏变为非全屏 if (!document.fullscreenElement && existElement) { - unmount('------全屏退出,清除完毕------') - return + unmount("------全屏退出,清除完毕------"); + return; } - mount('------进入全屏,bilibili-fullscreen-sc启动------') + mount("------进入全屏,bilibili-fullscreen-sc启动------"); }, - true, + true ); // video出现的时机 function loop() { - const video = getVideoDom() + const video = getVideoDom(); if (video) { - listenVideoSizeChange(video) + listenVideoSizeChange(video); // 如果切换清晰度,会销毁原本的video,然后新增video,所以原本在之前的video监听宽度已经没用了,需要重新监听 - monitorVideo(video) + monitorVideo(video); } else { window.requestAnimationFrame(loop); } @@ -54,41 +51,41 @@ export default defineContentScript({ window.requestAnimationFrame(loop); - function monitorVideo(video){ - const videoParent = video.parentNode + function monitorVideo(video) { + const videoParent = video.parentNode; // 当观察到变动时执行的回调函数 const callback = function (mutationsList) { for (const mutation of mutationsList) { - if (mutation.type === 'childList') { + if (mutation.type === "childList") { for (const i of mutation.addedNodes) { // 遍历新增的节点,取出新增的video节点 const addVideoDom = getAddVideoDom(i); - if(addVideoDom){ - resizeObserver?.disconnect() - listenVideoSizeChange(addVideoDom) + if (addVideoDom) { + resizeObserver?.disconnect(); + listenVideoSizeChange(addVideoDom); } } } } }; - + // 创建一个观察器实例并传入回调函数 const observer = new MutationObserver(callback); - + // 以上述配置开始观察目标节点 observer.observe(videoParent as Node, { attributes: false, childList: true, - subtree: true + subtree: true, }); } function getAddVideoDom(dom: HTMLElement | ChildNode): false | HTMLVideoElement { - if (dom.nodeName === 'VIDEO') { + if (dom.nodeName === "VIDEO") { return dom as HTMLVideoElement; } - + for (const i of dom.childNodes) { const iframeDom = getAddVideoDom(i); if (iframeDom) { @@ -98,66 +95,63 @@ export default defineContentScript({ return false; } - function mount(log: string) { - if (isMount) return - isMount = true + if (isMount) return; + isMount = true; - console.log(log) + console.log(log); if (isFirst) { - injectIframeCss() - isFirst = false + injectIframeCss(); + isFirst = false; } + existElement = document.createElement("div"); - existElement = document.createElement('div') - - const videoDom = getVideoDom() - console.log('videoParent', videoDom?.parentNode) - videoDom?.parentNode?.appendChild(existElement) - root = createRoot(existElement) - root.render() + const videoDom = getVideoDom(); + console.log("videoParent", videoDom?.parentNode); + videoDom?.parentNode?.appendChild(existElement); + root = createRoot(existElement); + root.render(); // setTimeout(() => { - // processData(testData) - // }, 1000) + // processData(testData); + // }, 1000); - getInfo() + getInfo(); } function unmount(log: string) { - if (!isMount) return - isMount = false - - console.log(log) - root?.unmount() - root = null - existElement?.parentNode?.removeChild(existElement) - existElement = null - liveWS?.close() - liveWS = null + if (!isMount) return; + isMount = false; + + console.log(log); + root?.unmount(); + root = null; + existElement?.parentNode?.removeChild(existElement); + existElement = null; + liveWS?.close(); + liveWS = null; } function listenVideoSizeChange(video: HTMLVideoElement) { - let lastTimePageFullScreen = false + let lastTimePageFullScreen = false; resizeObserver = new ResizeObserver((entries) => { // 当用户在popup关闭此功能后 - if (!switchState) - return + if (!switchState) return; for (const entry of entries) { - console.log('resizeObserver', entry) + console.log("resizeObserver", entry); if (entry.contentRect) { - const videoWidth = entry.contentRect?.width + const videoWidth = entry.contentRect?.width; if (videoWidth === window.innerWidth) { // 虽然宽度相同,但是有可能窗口resize也会进来,所以为了防止重复mount if (!lastTimePageFullScreen) { - lastTimePageFullScreen = true - mount('------进入了网页全屏模式------') + lastTimePageFullScreen = true; + mount("------进入了网页全屏模式------"); } } else if (lastTimePageFullScreen && videoWidth) { - lastTimePageFullScreen = false + lastTimePageFullScreen = false; // 执行卸载操作 - unmount('------退出了网页全屏模式------') + unmount("------退出了网页全屏模式------"); } } } @@ -167,75 +161,86 @@ export default defineContentScript({ } async function getInfo() { - const shortId = location.pathname.slice(1) + const shortId = location.pathname.slice(1); const roomId = await fetch(`https://api.live.bilibili.com/room/v1/Room/get_info?room_id=${shortId}`) - .then(response => response.json()) + .then((response) => response.json()) .then((res) => { - const { data: { room_id } = { room_id: 0 } } = res - return room_id - }) + const { data: { room_id } = { room_id: 0 } } = res; + return room_id; + }); const existingSCList = await fetch(`https://api.live.bilibili.com/xlive/web-room/v1/index/getInfoByRoom?room_id=${roomId}`) - .then(response => response.json()) + .then((response) => response.json()) .then((res) => { - const { data: { super_chat_info: { message_list } } } = res - return message_list - }) + const { + data: { + super_chat_info: { message_list }, + }, + } = res; + return message_list; + }); if (Array.isArray(existingSCList) && existingSCList.length) { - console.log('existingSCList', existingSCList) + console.log("existingSCList", existingSCList); for (let i of existingSCList) { - processData({ data: i }) + const time = i.end_time - i.start_time; + const delay = time - i.time; + processData({ data: { ...i, time, delay } }); } } - const key = await fetch(`https://api.live.bilibili.com/xlive/web-room/v1/index/getDanmuInfo?id=${roomId}`) - .then(response => response.json()) + .then((response) => response.json()) .then((res) => { - const { data: { token } = { token: '' } } = res - return token - }) + const { data: { token } = { token: "" } } = res; + return token; + }); liveWS = new LiveWS(roomId, { protover: 3, key, - }) - liveWS.on('SUPER_CHAT_MESSAGE', (res) => { - console.log('SC', res) - processData(res as DanmuDataProps) - }) + }); + liveWS.on("SUPER_CHAT_MESSAGE", (res) => { + console.log("SC", res); + processData(res as DanmuDataProps); + }); } // 获取跟video的dom(B站video的父级dom结构老是变,有病的!) function getVideoDom() { - return document.querySelector('video') || getVideoDomFromIframe()?.contentDocument?.querySelector('video') + return document.querySelector("video") || getVideoDomFromIframe()?.contentDocument?.querySelector("video"); } function getVideoDomFromIframe() { - return Array.from(document.querySelectorAll('iframe')).filter(item => item.allowFullscreen)[0] + return Array.from(document.querySelectorAll("iframe")).filter((item) => item.allowFullscreen)[0]; } // 有时候video在iframe里面,content-script.css的样式没法应用到里面去,所以将其应用到iframe head中 function injectIframeCss() { - const videoIframe = getVideoDomFromIframe() - if (videoIframe?.contentDocument?.querySelector('video')) { - console.log('--------video在iframe里面,需要手动在iframe中注入样式文件---------') - // @ts-ignore - console.log(`extension css文件路径:`, browser.runtime.getURL('/content-scripts/content.css')) - - isInIframe = true - const link = videoIframe.contentDocument.createElement('link'); - link.rel = 'stylesheet'; - + const videoIframe = getVideoDomFromIframe(); + if (videoIframe?.contentDocument?.querySelector("video")) { + console.log("--------video在iframe里面,需要手动在iframe中注入样式文件---------"); // @ts-ignore - link.href = browser.runtime.getURL('/content-scripts/content.css'); // 扩展中的 CSS 文件路径 + console.log( + `extension css文件路径:`, + // @ts-ignore + browser.runtime.getURL("/content-scripts/content.css") + ); + + isInIframe = true; + const link = videoIframe.contentDocument.createElement("link"); + link.rel = "stylesheet"; + + link.href = browser.runtime.getURL( + // @ts-ignore + "/content-scripts/content.css" + ); // 扩展中的 CSS 文件路径 videoIframe.contentDocument.head.appendChild(link); } } // 监听popup信息 browser.runtime.onMessage.addListener((request: { switchState: boolean }, _, sendResponse: (str: string) => void) => { - switchState = request.switchState - sendResponse('content got!') - }) + switchState = request.switchState; + sendResponse("content got!"); + }); }, }); diff --git a/utils/index.ts b/utils/index.ts index e927c6b..7fb94f7 100644 --- a/utils/index.ts +++ b/utils/index.ts @@ -42,6 +42,7 @@ export interface DanmuDataProps { background_color: string time: number id: number + delay: number // existing sc的属性 } [propNames: string]: any } @@ -66,6 +67,7 @@ function processData(res: DanmuDataProps) { background_color = '', time = 0, id = 0, + delay = 0 }, } = res @@ -81,6 +83,7 @@ function processData(res: DanmuDataProps) { background_color, time, id, + delay }) }