From 5dee536d2c6e579335cf3a8b9df63f0abe37fdb2 Mon Sep 17 00:00:00 2001 From: eeelester <11475842+eeelester@users.noreply.github.com> Date: Fri, 20 Dec 2024 02:07:28 +0800 Subject: [PATCH] =?UTF-8?q?style:=20=E6=A0=BC=E5=BC=8F=E5=8C=96=E4=BB=A3?= =?UTF-8?q?=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 89 +++--- .github/workflows/release.yml | 54 ++++ .release-it.json | 2 +- .vscode/settings.json | 40 ++- components/Popup/index.less | 231 +++++++------- components/ScList/index.less | 7 +- components/ScList/index.tsx | 294 +++++++++--------- entrypoints/background.ts | 52 ++-- entrypoints/content/comm.ts | 18 +- entrypoints/content/const.ts | 2 +- entrypoints/content/index.tsx | 48 +-- entrypoints/content/observePageFullScreen.tsx | 138 ++++---- entrypoints/content/types.d.ts | 37 +++ entrypoints/content/utils.tsx | 214 ++++++------- entrypoints/popup/App.tsx | 113 +++---- entrypoints/popup/main.tsx | 8 +- eslint.config.js | 5 +- package.json | 2 +- tsconfig.json | 2 +- utils/index.ts | 12 +- wxt.config.ts | 26 +- 21 files changed, 748 insertions(+), 646 deletions(-) create mode 100644 .github/workflows/release.yml create mode 100644 entrypoints/content/types.d.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6df2726..6d579b5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,54 +1,43 @@ -name: Deploy +name: CI + +on: + push: + branches: + - main + paths: + - "**.js" + - "**.ts" + - "**.tsx" + - "**.less" + - ".github/workflows/**" + pull_request: + branches: + - main + paths: + - "**.js" + - "**.ts" + - "**.tsx" + - "**.less" + - ".github/workflows/**" -on: - workflow_dispatch: - inputs: - version: - description: 'Version to release' - required: true jobs: - submit: - name: Release && Submit + CI: runs-on: ubuntu-latest - permissions: - contents: write steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - uses: pnpm/action-setup@v4 - with: - version: "latest" - - - uses: actions/setup-node@v4 - with: - node-version: 20 - cache: 'pnpm' - - - name: git config - run: | - git config --global user.name "${GITHUB_ACTOR}" - git config --global user.email "${GITHUB_ACTOR}@users.noreply.github.com" - - - name: Install dependencies - run: pnpm install - - - name: release - run: pnpm release -i=${{ github.event.inputs.version }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Zip extensions - run: pnpm zip - - - name: Submit to stores - run: | - pnpm wxt submit \ - --chrome-zip .output/*-chrome.zip - - env: - CHROME_EXTENSION_ID: ${{ secrets.CHROME_EXTENSION_ID }} - CHROME_CLIENT_ID: ${{ secrets.CHROME_CLIENT_ID }} - CHROME_CLIENT_SECRET: ${{ secrets.CHROME_CLIENT_SECRET }} - CHROME_REFRESH_TOKEN: ${{ secrets.CHROME_REFRESH_TOKEN }} + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: latest + run_install: true + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: pnpm + + - name: Lint + run: pnpm lint \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..082aebf --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,54 @@ +name: Release + +on: + workflow_dispatch: + inputs: + version: + description: Version to release + required: true +jobs: + submit: + name: Release && Submit + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: pnpm/action-setup@v4 + with: + version: latest + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: pnpm + + - name: git config + run: | + git config --global user.name "${GITHUB_ACTOR}" + git config --global user.email "${GITHUB_ACTOR}@users.noreply.github.com" + + - name: Install dependencies + run: pnpm install + + - name: release + run: pnpm release -i=${{ github.event.inputs.version }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Zip extensions + run: pnpm zip + + - name: Submit to stores + run: | + pnpm wxt submit \ + --chrome-zip .output/*-chrome.zip + + env: + CHROME_EXTENSION_ID: ${{ secrets.CHROME_EXTENSION_ID }} + CHROME_CLIENT_ID: ${{ secrets.CHROME_CLIENT_ID }} + CHROME_CLIENT_SECRET: ${{ secrets.CHROME_CLIENT_SECRET }} + CHROME_REFRESH_TOKEN: ${{ secrets.CHROME_REFRESH_TOKEN }} diff --git a/.release-it.json b/.release-it.json index 984e952..b4e2677 100644 --- a/.release-it.json +++ b/.release-it.json @@ -59,7 +59,7 @@ { "type": "chore", "section": "🎫 Chores | 其他更新" - } + } ] } } diff --git a/.vscode/settings.json b/.vscode/settings.json index 628b7aa..e82dacb 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,9 +1,6 @@ { - // Enable the ESlint flat config support - "eslint.experimental.useFlatConfig": true, - // Disable the default formatter, use eslint instead - "prettier.enable": true, + "prettier.enable": false, "editor.formatOnSave": true, // Auto fix @@ -14,16 +11,16 @@ // Silent the stylistic rules in you IDE, but still auto fix them "eslint.rules.customizations": [ - { "rule": "style/*", "severity": "off" }, - { "rule": "format/*", "severity": "off" }, - { "rule": "*-indent", "severity": "off" }, - { "rule": "*-spacing", "severity": "off" }, - { "rule": "*-spaces", "severity": "off" }, - { "rule": "*-order", "severity": "off" }, - { "rule": "*-dangle", "severity": "off" }, - { "rule": "*-newline", "severity": "off" }, - { "rule": "*quotes", "severity": "off" }, - { "rule": "*semi", "severity": "off" } + { "rule": "style/*", "severity": "off", "fixable": true }, + { "rule": "format/*", "severity": "off", "fixable": true }, + { "rule": "*-indent", "severity": "off", "fixable": true }, + { "rule": "*-spacing", "severity": "off", "fixable": true }, + { "rule": "*-spaces", "severity": "off", "fixable": true }, + { "rule": "*-order", "severity": "off", "fixable": true }, + { "rule": "*-dangle", "severity": "off", "fixable": true }, + { "rule": "*-newline", "severity": "off", "fixable": true }, + { "rule": "*quotes", "severity": "off", "fixable": true }, + { "rule": "*semi", "severity": "off", "fixable": true } ], // Enable eslint for all supported languages @@ -37,6 +34,17 @@ "markdown", "json", "jsonc", - "yaml" + "yaml", + "toml", + "xml", + "gql", + "graphql", + "astro", + "svelte", + "css", + "less", + "scss", + "pcss", + "postcss" ] -} +} \ No newline at end of file diff --git a/components/Popup/index.less b/components/Popup/index.less index 31e0ea4..6442a9a 100644 --- a/components/Popup/index.less +++ b/components/Popup/index.less @@ -1,138 +1,139 @@ @switchPrefixCls: rc-switch; -@duration: .3s; +@duration: 0.3s; .@{switchPrefixCls} { - position: relative; - display: inline-block; - box-sizing: border-box; - width: 44px; - height: 22px; - line-height: 20px; - padding: 0; - vertical-align: middle; - border-radius: 20px 20px; - border: 1px solid #ccc; - background-color: #ccc; + position: relative; + display: inline-block; + box-sizing: border-box; + width: 44px; + height: 22px; + line-height: 20px; + padding: 0; + vertical-align: middle; + border-radius: 20px 20px; + border: 1px solid #ccc; + background-color: #ccc; + cursor: pointer; + transition: all @duration cubic-bezier(0.35, 0, 0.25, 1); + overflow: hidden; + + &-inner-checked, + &-inner-unchecked { + color: #fff; + font-size: 12px; + position: absolute; + top: 0; + transition: left @duration cubic-bezier(0.35, 0, 0.25, 1); + } + + &-inner-checked { + left: 6px - 20px; + } + + &-inner-unchecked { + left: 24px; + } + + &:after { + position: absolute; + width: 18px; + height: 18px; + left: 2px; + top: 1px; + border-radius: 50% 50%; + background-color: #fff; + content: ' '; cursor: pointer; - transition: all @duration cubic-bezier(0.35, 0, 0.25, 1); - overflow: hidden; - - &-inner-checked, - &-inner-unchecked { - color: #fff; - font-size: 12px; - position: absolute; - top: 0; - transition: left @duration cubic-bezier(0.35, 0, 0.25, 1); - } + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.26); + transform: scale(1); + transition: left @duration cubic-bezier(0.35, 0, 0.25, 1); + animation-timing-function: cubic-bezier(0.35, 0, 0.25, 1); + animation-duration: @duration; + animation-name: rcSwitchOff; + } - &-inner-checked { - left: 6px - 20px; - } + &:hover:after { + transform: scale(1.1); + animation-name: rcSwitchOn; + } - &-inner-unchecked { - left: 24px; - } + &:focus { + box-shadow: 0 0 0 2px tint(#2db7f5, 80%); + outline: none; + } - &:after { - position: absolute; - width: 18px; - height: 18px; - left: 2px; - top: 1px; - border-radius: 50% 50%; - background-color: #fff; - content: " "; - cursor: pointer; - box-shadow: 0 2px 5px rgba(0, 0, 0, 0.26); - transform: scale(1); - transition: left @duration cubic-bezier(0.35, 0, 0.25, 1); - animation-timing-function: cubic-bezier(0.35, 0, 0.25, 1); - animation-duration: @duration; - animation-name: rcSwitchOff; - } + &-checked { + border: 1px solid #87d068; + background-color: #87d068; - &:hover:after { - transform: scale(1.1); - animation-name: rcSwitchOn; + .@{switchPrefixCls}-inner-checked { + left: 6px; } - &:focus { - box-shadow: 0 0 0 2px tint(#2db7f5, 80%); - outline: none; + .@{switchPrefixCls}-inner-unchecked { + left: 44px; } - &-checked { - border: 1px solid #87d068; - background-color: #87d068; - - .@{switchPrefixCls}-inner-checked { - left: 6px; - } + &:after { + left: 22px; + } + } - .@{switchPrefixCls}-inner-unchecked { - left: 44px; - } + &-disabled { + cursor: no-drop; + background: #ccc; + border-color: #ccc; - &:after { - left: 22px; - } + &:after { + background: #9e9e9e; + animation-name: none; + cursor: no-drop; } - &-disabled { - cursor: no-drop; - background: #ccc; - border-color: #ccc; - - &:after { - background: #9e9e9e; - animation-name: none; - cursor: no-drop; - } - - &:hover:after { - transform: scale(1); - animation-name: none; - } + &:hover:after { + transform: scale(1); + animation-name: none; } + } - &-label { - display: inline-block; - line-height: 20px; - font-size: 14px; - padding-left: 10px; - vertical-align: middle; - white-space: normal; - pointer-events: none; - user-select: text; - } + &-label { + display: inline-block; + line-height: 20px; + font-size: 14px; + padding-left: 10px; + vertical-align: middle; + white-space: normal; + pointer-events: none; + user-select: text; + } } @keyframes rcSwitchOn { - 0% { - transform: scale(1); - } + 0% { + transform: scale(1); + } - 50% { - transform: scale(1.25); - } + 50% { + transform: scale(1.25); + } - 100% { - transform: scale(1.1); - } + 100% { + transform: scale(1.1); + } } @keyframes rcSwitchOff { - 0% { - transform: scale(1.1); - } + 0% { + transform: scale(1.1); + } - 100% { - transform: scale(1); - } -}@switchPrefixCls: rc-switch; + 100% { + transform: scale(1); + } +} +@switchPrefixCls: rc-switch; -@duration: .3s; +@duration: 0.3s; .@{switchPrefixCls} { position: relative; @@ -152,7 +153,7 @@ &-inner-checked, &-inner-unchecked { - color:#fff; + color: #fff; font-size: 12px; position: absolute; top: 0; @@ -167,7 +168,7 @@ left: 24px; } - &:after{ + &:after { position: absolute; width: 18px; height: 18px; @@ -175,7 +176,7 @@ top: 1px; border-radius: 50% 50%; background-color: #fff; - content: " "; + content: ' '; cursor: pointer; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.26); transform: scale(1); @@ -207,23 +208,23 @@ left: 44px; } - &:after{ + &:after { left: 22px; } } - &-disabled{ + &-disabled { cursor: no-drop; background: #ccc; border-color: #ccc; - &:after{ + &:after { background: #9e9e9e; animation-name: none; cursor: no-drop; } - &:hover:after{ + &:hover:after { transform: scale(1); animation-name: none; } @@ -260,4 +261,4 @@ 100% { transform: scale(1); } -} \ No newline at end of file +} diff --git a/components/ScList/index.less b/components/ScList/index.less index fbd32d7..3abc8fb 100644 --- a/components/ScList/index.less +++ b/components/ScList/index.less @@ -14,7 +14,8 @@ .sc { width: @sc-width; border-radius: 6px; - font-family: Arial, "Microsoft YaHei", "Microsoft Sans Serif", "Microsoft SanSerf", "微软雅黑"; + font-family: Arial, 'Microsoft YaHei', 'Microsoft Sans Serif', + 'Microsoft SanSerf', '微软雅黑'; margin: 0 10px 4px 0; .top-container { @@ -69,7 +70,7 @@ } } - .close-btn{ + .close-btn { align-self: center; width: 16px; cursor: pointer; @@ -134,4 +135,4 @@ } } } -} \ No newline at end of file +} diff --git a/components/ScList/index.tsx b/components/ScList/index.tsx index 2af0f3f..3774b90 100644 --- a/components/ScList/index.tsx +++ b/components/ScList/index.tsx @@ -1,163 +1,167 @@ -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; - delay: number; + 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: unknown = JSON.parse(sessionStorage.getItem('deleteId') ?? 'null') + if (Array.isArray(existDeleteIdList) && existDeleteIdList.includes(scInfo.id)) { + 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: unknown = 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, - delay, - }) => ( - -
}> -
-
} - > -
-
-
-
-
- - {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/entrypoints/background.ts b/entrypoints/background.ts index e1c50bf..d08f15f 100644 --- a/entrypoints/background.ts +++ b/entrypoints/background.ts @@ -1,35 +1,35 @@ -import { MATCH_URL } from "@/constant"; +import { MATCH_URL } from '@/constant' import { changeIcon } from '@/utils' export default defineBackground(() => { - let userSwitchState: boolean | null = null - // tab处于活跃状态时获取当前tab的url,判断是否是b站直播页面,来显示icon的图标 - browser.tabs.onActivated.addListener(({ tabId }) => { - void (async () => { - const { url } = await browser.tabs.get(tabId) - processIcon(url) - })() - }) + let userSwitchState: boolean | null = null + // tab处于活跃状态时获取当前tab的url,判断是否是b站直播页面,来显示icon的图标 + browser.tabs.onActivated.addListener(({ tabId }) => { + void (async () => { + const { url } = await browser.tabs.get(tabId) + void processIcon(url) + })() + }) - // tab刷新及初次进来时获取当前tab的url,判断是否是b站直播页面,来显示icon的图标 - browser.tabs.onUpdated.addListener((_, { status }, { url }) => { - if (status === 'complete') - processIcon(url) - }) + // tab刷新及初次进来时获取当前tab的url,判断是否是b站直播页面,来显示icon的图标 + browser.tabs.onUpdated.addListener((_, { status }, { url }) => { + if (status === 'complete') + void processIcon(url) + }) - browser.runtime.onMessage.addListener(({ switchState }) => { - userSwitchState = switchState - }) + browser.runtime.onMessage.addListener(({ switchState }: { switchState: boolean }) => { + userSwitchState = switchState + }) - /** - * @description: - * url匹配,用户没动开关,true + /** + * @description: + * url匹配,用户没动开关,true url匹配,用户开,true url匹配,用户关,false url不匹配,false - * @return {*} - */ - function processIcon(url: string | undefined) { - url && MATCH_URL.test(url) && (userSwitchState || userSwitchState === null) ? changeIcon(true) : changeIcon(false) - } -}); + * @return {*} + */ + function processIcon(url: string | undefined) { + url && MATCH_URL.test(url) && (userSwitchState || userSwitchState === null) ? void changeIcon(true) : void changeIcon(false) + } +}) diff --git a/entrypoints/content/comm.ts b/entrypoints/content/comm.ts index fffae35..2050c39 100644 --- a/entrypoints/content/comm.ts +++ b/entrypoints/content/comm.ts @@ -1,11 +1,9 @@ +export let switchState: boolean = true -export let switchState: boolean = true; - - -export function popUpOnMessage(){ - // 监听popup信息 - browser.runtime.onMessage.addListener((request: { switchState: boolean }, _, sendResponse: (str: string) => void) => { - switchState = request.switchState; - sendResponse("content got!"); - }); -} \ No newline at end of file +export function popUpOnMessage() { + // 监听popup信息 + browser.runtime.onMessage.addListener((request: { switchState: boolean }, _, sendResponse: (str: string) => void) => { + switchState = request.switchState + sendResponse('content got!') + }) +} diff --git a/entrypoints/content/const.ts b/entrypoints/content/const.ts index c008387..7c74d52 100644 --- a/entrypoints/content/const.ts +++ b/entrypoints/content/const.ts @@ -1 +1 @@ -export const ALREADY_HAVE_IT = 'already have it' \ No newline at end of file +export const ALREADY_HAVE_IT = 'already have it' diff --git a/entrypoints/content/index.tsx b/entrypoints/content/index.tsx index 4862aad..2c884b6 100644 --- a/entrypoints/content/index.tsx +++ b/entrypoints/content/index.tsx @@ -1,29 +1,29 @@ -import { mount, unmount, existElement } from "./utils"; -import ObservePageFullScreen from "./observePageFullScreen"; -import { popUpOnMessage } from "./comm"; +import { existElement, mount, unmount } from './utils' +import ObservePageFullScreen from './observePageFullScreen' +import { popUpOnMessage } from './comm' export default defineContentScript({ - matches: ["https://live.bilibili.com/*"], - main() { - // 监听全屏模式 - document.addEventListener( - "fullscreenchange", - () => { - // 从全屏变为非全屏 - if (!document.fullscreenElement && existElement) { - unmount("------全屏退出,清除完毕------"); - return; - } + matches: ['https://live.bilibili.com/*'], + main() { + // 监听全屏模式 + document.addEventListener( + 'fullscreenchange', + () => { + // 从全屏变为非全屏 + if (!document.fullscreenElement && existElement) { + unmount('------全屏退出,清除完毕------') + return + } - mount("------进入全屏,bilibili-fullscreen-sc启动------"); - }, - true - ); + mount('------进入全屏,bilibili-fullscreen-sc启动------') + }, + true, + ) - // 监听网页全屏模式 - ObservePageFullScreen(); + // 监听网页全屏模式 + ObservePageFullScreen() - // 监听popup传输数据 - popUpOnMessage(); - }, -}); + // 监听popup传输数据 + popUpOnMessage() + }, +}) diff --git a/entrypoints/content/observePageFullScreen.tsx b/entrypoints/content/observePageFullScreen.tsx index d575f3a..ab50167 100644 --- a/entrypoints/content/observePageFullScreen.tsx +++ b/entrypoints/content/observePageFullScreen.tsx @@ -1,89 +1,89 @@ -import { mount, unmount, getVideoDom } from "./utils"; -import { switchState } from "./comm"; -import { ALREADY_HAVE_IT } from "./const"; +import { getVideoDom, mount, unmount } from './utils' +import { ALREADY_HAVE_IT } from './const' -let resizeObserver: ResizeObserver | null; +let resizeObserver: ResizeObserver | null export default function ObservePageFullScreen() { - // video出现的时机 - function loop() { - const video = getVideoDom(); - if (video) { - listenVideoSizeChange(video); - // 如果切换清晰度,会销毁原本的video,然后新增video,所以原本在之前的video监听宽度已经没用了,需要重新监听 - monitorVideo(video); - } else { - window.requestAnimationFrame(loop); - } + // video出现的时机 + function loop() { + const video = getVideoDom() + if (video) { + listenVideoSizeChange(video) + // 如果切换清晰度,会销毁原本的video,然后新增video,所以原本在之前的video监听宽度已经没用了,需要重新监听 + monitorVideo(video) + } + else { + window.requestAnimationFrame(loop) } + } - window.requestAnimationFrame(loop); + window.requestAnimationFrame(loop) } function listenVideoSizeChange(video: HTMLVideoElement) { - let lastTimePageFullScreen = false; - resizeObserver = new ResizeObserver((entries) => { - for (const entry of entries) { - console.log("resizeObserver", entry); - if (entry.contentRect) { - const videoWidth = entry.contentRect?.width; - if (videoWidth === window.innerWidth) { - // 虽然宽度相同,但是有可能窗口resize也会进来,所以为了防止重复mount - if (!lastTimePageFullScreen) { - // 全屏模式下跳另一种全屏,要做校验是否已经处于全屏模式 - if (mount("------进入了网页全屏模式------") !== ALREADY_HAVE_IT) lastTimePageFullScreen = true; - } - } else if (lastTimePageFullScreen && videoWidth) { - unmount("------退出了网页全屏模式------"); - lastTimePageFullScreen = false; - } - } + let lastTimePageFullScreen = false + resizeObserver = new ResizeObserver((entries) => { + for (const entry of entries) { + console.log('resizeObserver', entry) + if (entry.contentRect) { + const videoWidth = entry.contentRect?.width + if (videoWidth === window.innerWidth) { + // 虽然宽度相同,但是有可能窗口resize也会进来,所以为了防止重复mount + if (!lastTimePageFullScreen) { + // 全屏模式下跳另一种全屏,要做校验是否已经处于全屏模式 + if (mount('------进入了网页全屏模式------') !== ALREADY_HAVE_IT) + lastTimePageFullScreen = true + } } - }); + else if (lastTimePageFullScreen && videoWidth) { + unmount('------退出了网页全屏模式------') + lastTimePageFullScreen = false + } + } + } + }) - resizeObserver.observe(video); + resizeObserver.observe(video) } -function monitorVideo(video) { - const videoParent = video.parentNode; +function monitorVideo(video: HTMLVideoElement) { + const videoParent = video.parentNode - // 当观察到变动时执行的回调函数 - const callback = function (mutationsList) { - for (const mutation of mutationsList) { - if (mutation.type === "childList") { - for (const i of mutation.addedNodes) { - // 遍历新增的节点,取出新增的video节点 - const addVideoDom = getAddVideoDom(i); - if (addVideoDom) { - resizeObserver?.disconnect(); - listenVideoSizeChange(addVideoDom); - } - } - } + // 当观察到变动时执行的回调函数 + const callback = function (mutationsList: MutationRecord[]) { + for (const mutation of mutationsList) { + if (mutation.type === 'childList') { + for (const i of mutation.addedNodes) { + // 遍历新增的节点,取出新增的video节点 + const addVideoDom = getAddVideoDom(i) + if (addVideoDom) { + resizeObserver?.disconnect() + listenVideoSizeChange(addVideoDom) + } } - }; + } + } + } - // 创建一个观察器实例并传入回调函数 - const observer = new MutationObserver(callback); + // 创建一个观察器实例并传入回调函数 + const observer = new MutationObserver(callback) - // 以上述配置开始观察目标节点 - observer.observe(videoParent as Node, { - attributes: false, - childList: true, - subtree: true, - }); + // 以上述配置开始观察目标节点 + observer.observe(videoParent as Node, { + attributes: false, + childList: true, + subtree: true, + }) } -function getAddVideoDom(dom: HTMLElement | ChildNode): false | HTMLVideoElement { - if (dom.nodeName === "VIDEO") { - return dom as HTMLVideoElement; - } +function getAddVideoDom(dom: Node | ChildNode): false | HTMLVideoElement { + if (dom.nodeName === 'VIDEO') + return dom as HTMLVideoElement - for (const i of dom.childNodes) { - const iframeDom = getAddVideoDom(i); - if (iframeDom) { - return iframeDom; - } - } - return false; + for (const i of dom.childNodes) { + const iframeDom = getAddVideoDom(i) + if (iframeDom) + return iframeDom + } + return false } diff --git a/entrypoints/content/types.d.ts b/entrypoints/content/types.d.ts new file mode 100644 index 0000000..f98e605 --- /dev/null +++ b/entrypoints/content/types.d.ts @@ -0,0 +1,37 @@ +import type { DanmuDataProps } from '@/utils' + +interface BilibiliResponseBasic { + code: number + message: string + msg: string + ttl: number +} + +export interface RoomInfo extends BilibiliResponseBasic { + data: { + room_id: number + } +} + +type DanmuData = Pick['data'] +interface MessageList extends DanmuData { + time: number + start_time: number + end_time: number +} + +export interface RoomDetailInfo extends BilibiliResponseBasic { + data: { + super_chat_info: { + message_list: MessageList[] + } + } +} + +export interface DanmuInfo extends BilibiliResponseBasic { + data: { + token: string + } +} + +export type Status = 'active' | 'inactive' | 'pending' diff --git a/entrypoints/content/utils.tsx b/entrypoints/content/utils.tsx index 456198c..cf89820 100644 --- a/entrypoints/content/utils.tsx +++ b/entrypoints/content/utils.tsx @@ -1,135 +1,139 @@ -import { createRoot } from "react-dom/client"; -import type { Root } from "react-dom/client"; -import SCList from "@/components/ScList"; -import { LiveWS } from "bilibili-live-ws"; -import type { DanmuDataProps } from "@/utils"; -import { processData } from "@/utils"; -import { ALREADY_HAVE_IT } from "./const"; -import { switchState } from "./comm"; +import { createRoot } from 'react-dom/client' +import type { Root } from 'react-dom/client' +import { LiveWS } from 'bilibili-live-ws' +import { ALREADY_HAVE_IT } from './const' +import { switchState } from './comm' +import type { DanmuInfo, RoomDetailInfo, RoomInfo } from './types' +import SCList from '@/components/ScList' +import type { DanmuDataProps } from '@/utils' +import { processData } from '@/utils' + // import { testData } from "../dev/testData"; -let isFirst = true; -let isMount = false; -let isInIframe = false; -let root: Root | null; -let liveWS: LiveWS | null; +let isFirst = true +let isMount = false +let isInIframe = false +let root: Root | null +let liveWS: LiveWS | null -export let existElement: HTMLElement | null; +export let existElement: HTMLElement | null export function mount(log: string) { - if (isMount) return ALREADY_HAVE_IT; - // 当用户在popup关闭此功能后 - if (!switchState) return; + if (isMount) + return ALREADY_HAVE_IT + // 当用户在popup关闭此功能后 + if (!switchState) + return - isMount = true; + isMount = true - console.log(log); + console.log(log) - if (isFirst) { - injectIframeCss(); - isFirst = false; - } + if (isFirst) { + 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(); - // setTimeout(() => { - // processData(testData); - // }, 1000); + const videoDom = getVideoDom() + console.log('videoParent', videoDom?.parentNode) + videoDom?.parentNode?.appendChild(existElement) + root = createRoot(existElement) + root.render() + // setTimeout(() => { + // processData(testData); + // }, 1000); - getInfo(); + void getInfo() } export 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 } // 有时候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文件路径:`, - // @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); - } + const videoIframe = getVideoDomFromIframe() + if (videoIframe?.contentDocument?.querySelector('video')) { + console.log('--------video在iframe里面,需要手动在iframe中注入样式文件---------') + + console.log( + `extension css文件路径:`, + // @ts-expect-error: 实际是有的 + browser.runtime.getURL('/content-scripts/content.css'), + ) + + isInIframe = true + const link = videoIframe.contentDocument.createElement('link') + link.rel = 'stylesheet' + + link.href = browser.runtime.getURL( + // @ts-expect-error: 实际是有的 + '/content-scripts/content.css', + ) // 扩展中的 CSS 文件路径 + videoIframe.contentDocument.head.appendChild(link) + } } async function getInfo() { - 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((res) => { - 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((res) => { - const { - data: { - super_chat_info: { message_list }, - }, - } = res; - return message_list; - }); - if (Array.isArray(existingSCList) && existingSCList.length) { - console.log("existingSCList", existingSCList); - for (let i of existingSCList) { - const time = i.end_time - i.start_time; - const delay = time - i.time; - processData({ data: { ...i, time, delay } }); - } + 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((res: RoomInfo) => { + 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((res: RoomDetailInfo) => { + const { + data: { + super_chat_info: { message_list }, + }, + } = res + return message_list + }) + if (Array.isArray(existingSCList) && existingSCList.length) { + console.log('existingSCList', existingSCList) + for (const i of existingSCList) { + 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((res) => { - 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); - }); + } + const key = await fetch(`https://api.live.bilibili.com/xlive/web-room/v1/index/getDanmuInfo?id=${roomId}`) + .then(response => response.json()) + .then((res: DanmuInfo) => { + const { data: { token } = { token: '' } } = res + return token + }) + liveWS = new LiveWS(roomId, { + protover: 3, + key, + }) + liveWS.on('SUPER_CHAT_MESSAGE', (res: DanmuDataProps) => { + console.log('SC', res) + processData(res) + }) } // 获取跟video的dom(B站video的父级dom结构老是变,有病的!) export 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] } diff --git a/entrypoints/popup/App.tsx b/entrypoints/popup/App.tsx index da92949..b57661b 100644 --- a/entrypoints/popup/App.tsx +++ b/entrypoints/popup/App.tsx @@ -1,53 +1,56 @@ -import Wrong from "@/components/Wrong"; -import Popup from "@/components/Popup"; -import { MATCH_URL, POPUP_INITIAL_STATE } from "@/constant"; -import { changeIcon } from "@/utils"; -import { useLayoutEffect } from "react"; -import { usePrevious } from "./hook"; +import { useLayoutEffect } from 'react' +import { usePrevious } from './hook' +import Wrong from '@/components/Wrong' +import Popup from '@/components/Popup' +import { MATCH_URL, POPUP_INITIAL_STATE } from '@/constant' +import { changeIcon } from '@/utils' function App() { - const [isMatch, setIsMatch] = useState(true); - const [switchState, setSwitchState] = useState(POPUP_INITIAL_STATE); - const prevSwitchState = usePrevious(switchState); + const [isMatch, setIsMatch] = useState(true) + const [switchState, setSwitchState] = useState(POPUP_INITIAL_STATE) + const prevSwitchState = usePrevious(switchState) - useLayoutEffect(() => { - (async () => { - const [tab] = await browser.tabs.query({ - active: true, - currentWindow: true, - }); - console.log("tab", tab); - setIsMatch(MATCH_URL.test(tab.url as string)); - })(); - }, [setIsMatch]); + useLayoutEffect(() => { + void (async () => { + const [tab] = await browser.tabs.query({ + active: true, + currentWindow: true, + }) + console.log('tab', tab) + setIsMatch(MATCH_URL.test(tab.url as string)) + })() + }, [setIsMatch]) - useLayoutEffect(() => { - (async () => { - console.log("changeIcon", isMatch); - if (!isMatch) { - changeIcon(false); - } else { - let tmp: boolean | null | undefined = await storage.getItem("session:switchState"); - setSwitchState(tmp ?? POPUP_INITIAL_STATE); - } - })(); - }, [isMatch]); + useLayoutEffect(() => { + void (async () => { + console.log('changeIcon', isMatch) + if (!isMatch) { + await changeIcon(false) + } + else { + const tmp: boolean | null | undefined = await storage.getItem('session:switchState') + setSwitchState(tmp ?? POPUP_INITIAL_STATE) + } + })() + }, [isMatch]) - /** - * @description:开关,用户可以自行控制是否开启全屏显示SC,因为用POPUP_INITIAL_STATE值作为useState初始值,第一次进来会触发useEffect,所以使用了usePrevious获取前值做判断 - * @param {*} props - * @return {*} - */ - useEffect(() => { - if (switchState && !prevSwitchState) onSwitch(true); + /** + * @description:开关,用户可以自行控制是否开启全屏显示SC,因为用POPUP_INITIAL_STATE值作为useState初始值,第一次进来会触发useEffect,所以使用了usePrevious获取前值做判断 + * @param {*} props + * @return {*} + */ + useEffect(() => { + if (switchState && !prevSwitchState) + void onSwitch(true) - if (!switchState && prevSwitchState) onSwitch(false); + if (!switchState && prevSwitchState) + void onSwitch(false) - console.log("switchState/prevSwitchState", switchState, prevSwitchState); - }, [switchState]); + console.log('switchState/prevSwitchState', switchState, prevSwitchState) + }, [switchState]) - // TODO: 开启后增加提示 - return isMatch ? : ; + // TODO: 开启后增加提示 + return isMatch ? : } /** @@ -56,21 +59,21 @@ function App() { * @return {*} */ async function onSwitch(switchState: boolean) { - const [tab] = await browser.tabs.query({ - active: true, - currentWindow: true, - }); - browser.tabs.sendMessage(tab.id as number, { - switchState, - }); + const [tab] = await browser.tabs.query({ + active: true, + currentWindow: true, + }) + await browser.tabs.sendMessage(tab.id as number, { + switchState, + }) - // 存着,不然下次点击popup就没有了 - storage.setItem("session:switchState", switchState); + // 存着,不然下次点击popup就没有了 + await storage.setItem('session:switchState', switchState) - // 发送给background,这个开关是用户操作,级别最高,background的根据url切换图标状态要在这之下 - browser.runtime.sendMessage({ switchState }); + // 发送给background,这个开关是用户操作,级别最高,background的根据url切换图标状态要在这之下 + await browser.runtime.sendMessage({ switchState }) - changeIcon(switchState); + void changeIcon(switchState) } -export default App; +export default App diff --git a/entrypoints/popup/main.tsx b/entrypoints/popup/main.tsx index e5775c0..e63eef4 100644 --- a/entrypoints/popup/main.tsx +++ b/entrypoints/popup/main.tsx @@ -1,9 +1,9 @@ -import React from 'react'; -import ReactDOM from 'react-dom/client'; -import App from './App.tsx'; +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App.tsx' ReactDOM.createRoot(document.getElementById('root')!).render( , -); +) diff --git a/eslint.config.js b/eslint.config.js index 328bf3e..464ee7f 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -19,13 +19,16 @@ module.exports = antfu( }, ignores: [ '*.config.js', + '*.config.ts', 'test/**' ] }, { files: ['**/*.tsx', '**/*.ts'], rules: { - "react-hooks/exhaustive-deps": "warn" + "react-hooks/exhaustive-deps": "warn", + "no-console":"off", + "import/no-mutable-exports": "warn" }, }, ...compat.config({ diff --git a/package.json b/package.json index e78e95d..6145a0d 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "zip": "wxt zip", "compile": "tsc --noEmit", "postinstall": "wxt prepare", - "lint": "eslint src/** --fix", + "lint": "eslint . --fix", "test": "jest", "prepare": "husky install", "release": "release-it" diff --git a/tsconfig.json b/tsconfig.json index e94b1a4..b910b8c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,8 +1,8 @@ { "extends": "./.wxt/tsconfig.json", "compilerOptions": { - "allowImportingTsExtensions": true, "jsx": "react-jsx", + "allowImportingTsExtensions": true, "noImplicitAny": false } } diff --git a/utils/index.ts b/utils/index.ts index 7fb94f7..1dae8ee 100644 --- a/utils/index.ts +++ b/utils/index.ts @@ -6,9 +6,9 @@ import { WS_SC_EVENT } from '@/constant' * @param {boolean} switchState * @return {*} */ -function changeIcon(switchState: boolean) { +async function changeIcon(switchState: boolean) { if (switchState) { - browser.action.setIcon({ + await browser.action.setIcon({ path: { 16: '/icons/icon-able-16.png', 32: '/icons/icon-able-32.png', @@ -17,7 +17,7 @@ function changeIcon(switchState: boolean) { }) } else { - browser.action.setIcon({ + await browser.action.setIcon({ path: { 16: '/icons/icon-16.png', 32: '/icons/icon-32.png', @@ -42,7 +42,7 @@ export interface DanmuDataProps { background_color: string time: number id: number - delay: number // existing sc的属性 + delay: number // existing sc的属性 } [propNames: string]: any } @@ -67,7 +67,7 @@ function processData(res: DanmuDataProps) { background_color = '', time = 0, id = 0, - delay = 0 + delay = 0, }, } = res @@ -83,7 +83,7 @@ function processData(res: DanmuDataProps) { background_color, time, id, - delay + delay, }) } diff --git a/wxt.config.ts b/wxt.config.ts index b28c4b5..9fb044d 100644 --- a/wxt.config.ts +++ b/wxt.config.ts @@ -1,31 +1,31 @@ -import { defineConfig } from 'wxt'; +import { defineConfig } from 'wxt' // See https://wxt.dev/api/config.html export default defineConfig({ modules: ['@wxt-dev/module-react'], manifest: { name: 'B站SC助手', - permissions: ['storage','tabs'], + permissions: ['storage', 'tabs'], icons: { - "16": "./icons/icon-able-16.png", - "32": "./icons/icon-able-32.png", - "48": "./icons/icon-able-48.png" - } + 16: './icons/icon-able-16.png', + 32: './icons/icon-able-32.png', + 48: './icons/icon-able-48.png', + }, }, runner: { - startUrls: ["https://live.bilibili.com/7777"], + startUrls: ['https://live.bilibili.com/7777'], }, hooks: { build: { - manifestGenerated(_,manifest: any) { - manifest.action.default_title = '在B站看直播全屏时展示SC'; + manifestGenerated(_, manifest: any) { + manifest.action.default_title = '在B站看直播全屏时展示SC' manifest.web_accessible_resources = [ { - "resources": [ "*.css" ], - "matches": [ "https://live.bilibili.com/*" ] - } + resources: ['*.css'], + matches: ['https://live.bilibili.com/*'], + }, ] }, }, }, -}); +})