From f50a2e3abd612003946fc48cc5eea0a52265912c Mon Sep 17 00:00:00 2001 From: IndieKKY Date: Thu, 3 Oct 2024 23:38:18 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84=E6=B6=88=E6=81=AF=E9=80=9A?= =?UTF-8?q?=E4=BF=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 5 +- pnpm-lock.yaml | 29 +- src/App.tsx | 8 +- src/biz/MoreBtn.tsx | 8 +- src/chrome/background.ts | 115 ++++-- src/const.tsx | 25 +- src/hooks/useMessageService.ts | 101 +++++ src/hooks/useSubtitle.ts | 4 +- src/hooks/useSubtitleService.ts | 84 ++--- src/hooks/useTranslate.ts | 12 +- src/inject/inject.ts | 646 +++++++++++++++++++------------- src/typings.d.ts | 20 + src/util/biz_util.ts | 36 ++ 13 files changed, 719 insertions(+), 374 deletions(-) create mode 100644 src/hooks/useMessageService.ts diff --git a/package.json b/package.json index a7e4c5a..47a7df5 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "@crxjs/vite-plugin": "^1.0.14", "@kky002/kky-hooks": "^1.2.1", "@kky002/kky-ui": "^1.0.9", - "@kky002/kky-util": "^1.4.2", + "@kky002/kky-util": "^1.13.13", "@logto/react": "1.0.0-beta.13", "@popperjs/core": "^2.11.6", "@reduxjs/toolkit": "^1.8.5", @@ -28,6 +28,7 @@ "less": "^4.1.3", "lodash-es": "^4.17.21", "pako": "^2.1.0", + "postmessage-promise": "^3.2.1", "qs": "^6.11.0", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -46,10 +47,10 @@ "devDependencies": { "@tailwindcss/line-clamp": "^0.4.2", "@tailwindcss/typography": "^0.5.8", - "@types/node": "^20.8.10", "@types/chrome": "^0.0.203", "@types/js-search": "^1.4.0", "@types/lodash-es": "^4.17.6", + "@types/node": "^20.8.10", "@types/pako": "^2.0.0", "@types/qs": "^6.9.7", "@types/react": "^18.0.20", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 336a1f0..ef241f8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,8 +18,8 @@ importers: specifier: ^1.0.9 version: 1.0.9 '@kky002/kky-util': - specifier: ^1.4.2 - version: 1.4.2 + specifier: ^1.13.13 + version: 1.13.13 '@logto/react': specifier: 1.0.0-beta.13 version: 1.0.0-beta.13(react@18.2.0) @@ -53,6 +53,9 @@ importers: pako: specifier: ^2.1.0 version: 2.1.0 + postmessage-promise: + specifier: ^3.2.1 + version: 3.2.1 qs: specifier: ^6.11.0 version: 6.11.0 @@ -390,8 +393,8 @@ packages: '@kky002/kky-ui@1.0.9': resolution: {integrity: sha512-pepfRcLfC1eIQ1lsSJLWNr4PgdLqFLuvQMlitJy7W668yZ7qu8yAHSjg8A20R7HB4mFkJ+B96WETalOar1e/kA==} - '@kky002/kky-util@1.4.2': - resolution: {integrity: sha512-gpZHWuCBBgYV1rnZ07FhriCR7x2228LOnf6PI6nyfWXxYYy1RQ8MZcdegbBsi/HRmh7EsW1yPqq85pwNLX0B9w==} + '@kky002/kky-util@1.13.13': + resolution: {integrity: sha512-DvePr8J7dyOaVteU/bskuoL3noHiOKpX3IGhN1h0v/Nt/fGI/tA1JKwUVrg4PE89xKAhH7c2Z+RHLa8zj7w7ng==} '@logto/browser@1.0.0-beta.13': resolution: {integrity: sha512-ddAVggFcbS9yfG8Gvn2xknE2NZd6+lGxOQ6UbjIJKsYBAsJG95u1ITYaP7tNSDdxqZPmSBGXp4rfsQB+u0JPJQ==} @@ -2005,6 +2008,9 @@ packages: resolution: {integrity: sha512-h+pbPsyhlYj6N2ozBmHhHrs9DzGmbaarbLvWipMRO7RLS+v4onj26MPFXA5OBYFxyqYhUJK456SwDcY9H2/zsA==} engines: {node: ^10 || ^12 || >=14} + postmessage-promise@3.2.1: + resolution: {integrity: sha512-cSs5eg+DvBQIdIQK9Cimd1wB2eb85xlzJXkJwm6jYNcTlsiwTFXvdyF/69JFozX6vIkdYz2Jv31W+BvSKQXNVg==} + prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -2384,6 +2390,10 @@ packages: engines: {node: '>=4.2.0'} hasBin: true + ua-parser-js@1.0.39: + resolution: {integrity: sha512-k24RCVWlEcjkdOxYmVJgeD/0a1TiSpqLg+ZalVGV9lsnr4yqu0w7tX/x2xX6G4zpkgQnRf89lxuZ1wsbjXM8lw==} + hasBin: true + uberproto@1.2.0: resolution: {integrity: sha512-pGtPAQmLwh+R9w81WVHzui1FfedpQWQpiaIIfPCwhtsBez4q6DYbJFfyXPVHPUTNFnedAvNEnkoFiLuhXIR94w==} @@ -2804,7 +2814,7 @@ snapshots: '@kky002/kky-hooks@1.2.1': dependencies: - '@kky002/kky-util': 1.4.2 + '@kky002/kky-util': 1.13.13 ahooks: 3.7.5(react@18.2.0) lodash-es: 4.17.21 react: 18.2.0 @@ -2813,10 +2823,11 @@ snapshots: dependencies: react: 18.2.0 - '@kky002/kky-util@1.4.2': + '@kky002/kky-util@1.13.13': dependencies: lodash-es: 4.17.21 qs: 6.11.0 + ua-parser-js: 1.0.39 '@logto/browser@1.0.0-beta.13': dependencies: @@ -4657,6 +4668,10 @@ snapshots: picocolors: 1.0.0 source-map-js: 1.0.2 + postmessage-promise@3.2.1: + dependencies: + '@babel/runtime': 7.19.0 + prelude-ls@1.2.1: {} prop-types@15.8.1: @@ -5045,6 +5060,8 @@ snapshots: typescript@4.8.3: {} + ua-parser-js@1.0.39: {} + uberproto@1.2.0: {} unbox-primitive@1.0.2: diff --git a/src/App.tsx b/src/App.tsx index 3487547..33e8ab3 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -6,16 +6,17 @@ import Header from './biz/Header' import Body from './biz/Body' import useSubtitleService from './hooks/useSubtitleService' import {cloneDeep} from 'lodash-es' -import {EVENT_EXPAND, PAGE_MAIN, PAGE_SETTINGS, STORAGE_ENV, STORAGE_TEMP} from './const' +import {EVENT_EXPAND, MESSAGE_TO_INJECT_FOLD, PAGE_MAIN, PAGE_SETTINGS, STORAGE_ENV, STORAGE_TEMP} from './const' import {EventBusContext} from './Router' import useTranslateService from './hooks/useTranslateService' import Settings from './biz/Settings' -import classNames from 'classnames' import {handleJson} from '@kky002/kky-util' import {useLocalStorage} from '@kky002/kky-hooks' import {Toaster} from 'react-hot-toast' import {setTheme} from './util/biz_util' +import {sendInject} from './util/biz_util' import useSearchService from './hooks/useSearchService' +import useMessageService from './hooks/useMessageService' function App() { const dispatch = useAppDispatch() @@ -29,7 +30,7 @@ function App() { const foldCallback = useCallback(() => { dispatch(setFold(!fold)) dispatch(setPage(PAGE_MAIN)) - window.parent.postMessage({type: 'fold', fold: !fold}, '*') + sendInject(MESSAGE_TO_INJECT_FOLD, {fold: !fold}) }, [dispatch, fold]) // handle event @@ -74,6 +75,7 @@ function App() { useSubtitleService() useTranslateService() useSearchService() + useMessageService() return
{ }, [curSummaryType, data, downloadType, segments, title, url]) const downloadAudioCallback = useCallback(() => { - window.parent.postMessage({ - type: 'downloadAudio', - }, '*') + sendInject(MESSAGE_TO_INJECT_DOWNLOAD_AUDIO, {}) }, []) const selectCallback = useCallback((e: any) => { diff --git a/src/chrome/background.ts b/src/chrome/background.ts index 005c9e5..cda24e4 100644 --- a/src/chrome/background.ts +++ b/src/chrome/background.ts @@ -1,59 +1,37 @@ import {v4} from 'uuid' import {handleTask, initTaskService, tasksMap} from './taskService' +import {MESSAGE_TARGET_EXTENSION, MESSAGE_TO_EXTENSION_ADD_TASK, MESSAGE_TO_EXTENSION_GET_TASK} from '@/const' -/** - * 消息处理入口 - * 注意:需要异步sendResponse时返回true - */ -chrome.runtime.onMessage.addListener((event, sender, sendResponse) => { - console.debug('收到请求: ', event) - if (event.type === 'p') { // 发出http请求 - const {url, options} = event - // 发出请求 - fetch('http://localhost:27081/oproxy/p', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - url, - options, - }), - }).then(async res => await res.json()).then(sendResponse).catch(console.error) - return true - } else if (event.type === 'syncGet') { // sync.get - chrome.storage.sync.get(event.keys, data => { - sendResponse(data) - }) - return true - } else if (event.type === 'syncSet') { // sync.set - chrome.storage.sync.set(event.items).catch(console.error) - } else if (event.type === 'syncRemove') { // sync.remove - chrome.storage.sync.remove(event.keys).catch(console.error) - } else if (event.type === 'addTask') { +const debug = (...args: any[]) => { + console.debug('[Extension]', ...args) +} + +const methods: { + [key: string]: (params: any, context: MethodContext) => Promise +} = { + [MESSAGE_TO_EXTENSION_ADD_TASK]: async (params, context) => { // 新建任务 const task: Task = { id: v4(), startTime: Date.now(), status: 'pending', - def: event.taskDef, + def: params.taskDef, } tasksMap.set(task.id, task) // 立即触发任务 handleTask(task).catch(console.error) + return task + }, + [MESSAGE_TO_EXTENSION_GET_TASK]: async (params, context) => { // 返回任务信息 - sendResponse(task) - } else if (event.type === 'getTask') { - // 返回任务信息 - const taskId = event.taskId + const taskId = params.taskId const task = tasksMap.get(taskId) if (task == null) { - sendResponse({ + return { code: 'not_found', - }) - return + } } // 检测删除缓存 @@ -62,9 +40,68 @@ chrome.runtime.onMessage.addListener((event, sender, sendResponse) => { } // 返回任务 - sendResponse({ + return { code: 'ok', task, + } + }, +} + +/** + * Note: Return true when sending a response asynchronously. + */ +chrome.runtime.onMessage.addListener((event: MessageData, sender: chrome.runtime.MessageSender, sendResponse: (result: any) => void) => { + debug((sender.tab != null) ? `tab ${sender.tab.url ?? ''} => ` : 'extension => ', event) + + // legacy + if (event.type === 'syncGet') { // sync.get + chrome.storage.sync.get(event.keys, data => { + sendResponse(data) + }) + return true + } else if (event.type === 'syncSet') { // sync.set + chrome.storage.sync.set(event.items).catch(console.error) + return + } else if (event.type === 'syncRemove') { // sync.remove + chrome.storage.sync.remove(event.keys).catch(console.error) + return + } + + // check event target + if (event.target !== MESSAGE_TARGET_EXTENSION) return + + const method = methods[event.method] + if (method != null) { + method(event.params, { + event, + sender, + }).then(data => sendResponse({ + success: true, + code: 200, + data, + })).catch(err => { + console.error(err) + let message + if (err instanceof Error) { + message = err.message + } else if (typeof err === 'string') { + message = err + } else { + message = 'error: ' + JSON.stringify(err) + } + sendResponse({ + success: false, + code: 500, + message, + }) + }) + return true + } else { + console.error('Unknown method:', event.method) + sendResponse({ + success: false, + code: 501, + message: 'Unknown method: ' + event.method, }) } }) diff --git a/src/const.tsx b/src/const.tsx index e97242b..3b60553 100644 --- a/src/const.tsx +++ b/src/const.tsx @@ -1,3 +1,26 @@ +export const MESSAGE_TARGET_EXTENSION = 'BilibiliExtension' +export const MESSAGE_TARGET_INJECT = 'BilibiliInject' +export const MESSAGE_TARGET_APP = 'BilibiliAPP' + +export const MESSAGE_TO_EXTENSION_ADD_TASK = 'addTask' +export const MESSAGE_TO_EXTENSION_GET_TASK = 'getTask' + +export const MESSAGE_TO_INJECT_FOLD = 'fold' +export const MESSAGE_TO_INJECT_MOVE = 'move' +export const MESSAGE_TO_INJECT_PLAY = 'play' +export const MESSAGE_TO_INJECT_DOWNLOAD_AUDIO = 'downloadAudio' +export const MESSAGE_TO_INJECT_GET_VIDEO_STATUS = 'getVideoStatus' +export const MESSAGE_TO_INJECT_GET_VIDEO_ELEMENT_INFO = 'getVideoElementInfo' +export const MESSAGE_TO_INJECT_REFRESH_VIDEO_INFO = 'refreshVideoInfo' +export const MESSAGE_TO_INJECT_UPDATETRANSRESULT = 'updateTransResult' +export const MESSAGE_TO_INJECT_HIDE_TRANS = 'hideTrans' +export const MESSAGE_TO_INJECT_GET_SUBTITLE = 'getSubtitle' + +export const MESSAGE_TO_APP_SET_INFOS = 'setInfos' +export const MESSAGE_TO_APP_SET_VIDEO_INFO = 'setVideoInfo' + +export const EVENT_EXPAND = 'expand' + export const APP_DOM_ID = 'bilibili-subtitle' export const IFRAME_ID = 'bilibili-subtitle-iframe' @@ -189,8 +212,6 @@ Answer: `, } -export const EVENT_EXPAND = 'expand' - export const TASK_EXPIRE_TIME = 15*60*1000 export const PAGE_MAIN = 'main' diff --git a/src/hooks/useMessageService.ts b/src/hooks/useMessageService.ts new file mode 100644 index 0000000..7138ed2 --- /dev/null +++ b/src/hooks/useMessageService.ts @@ -0,0 +1,101 @@ +import {useCallback, useContext, useEffect} from 'react' +import { + MESSAGE_TARGET_APP, + MESSAGE_TARGET_EXTENSION, + MESSAGE_TARGET_INJECT, + MESSAGE_TO_APP_SET_INFOS, + MESSAGE_TO_APP_SET_VIDEO_INFO, +} from '@/const' +import {debug} from '@/util/biz_util' +import {callServer, PostMessagePayload, PostMessageResponse} from 'postmessage-promise' +import {useAppDispatch} from '../hooks/redux' +import {Waiter} from '@kky002/kky-util' +import {setInfos, setTitle, setUrl, setCurInfo, setCurFetched, setData} from '@/redux/envReducer' + +let postInjectMessage: (method: string, params: PostMessagePayload) => Promise | undefined + +export const injectWaiter = new Waiter(() => ({ + finished: postInjectMessage != null, + data: postInjectMessage +}), 100, 15000) + +const useMessageService = () => { + const dispatch = useAppDispatch() + const path = 'app' //useAppSelector(state => state.env.path) + + const messageHandler = useCallback((method: string, params: any, from: string, context: any): boolean => { + switch (method) { + case MESSAGE_TO_APP_SET_INFOS: + dispatch(setInfos(params.infos)) + dispatch(setCurInfo(undefined)) + dispatch(setCurFetched(false)) + dispatch(setData(undefined)) + break + case MESSAGE_TO_APP_SET_VIDEO_INFO: + dispatch(setInfos(params.infos)) + dispatch(setUrl(params.url)) + dispatch(setTitle(params.title)) + console.debug('video title: ', params.title) + break + default: + debug('unknown message method: ', method) + return false + } + return true + }, [dispatch]) + + // connect to inject + useEffect(() => { + if (path !== 'app') return + + let destroyFunc: (() => void) | undefined + + const serverObject = { + server: window.parent, // openedWindow / window.parent / window.opener; + origin: '*', // target-window's origin or * + } + const options = {} + callServer(serverObject, options).then(e => { + const { postMessage, listenMessage, destroy } = e + postInjectMessage = postMessage + destroyFunc = destroy + + listenMessage((method, params, sendResponse) => { + debug('inject => ', method, params) + + const success = messageHandler(method, params, MESSAGE_TARGET_INJECT, {}) + sendResponse({ + success, + code: success ? 200 : 500 + }) + }) + + debug('message ready') + }).catch(console.error) + + return () => { + destroyFunc?.() + } + }, [messageHandler, path]) + + const extensionMessageCallback = useCallback((event: MessageData, sender: chrome.runtime.MessageSender, sendResponse: (response?: any) => void) => { + debug((sender.tab != null) ? `tab ${sender.tab.url??''} => ` : 'extension => ', JSON.stringify(event)) + + // check event target + if (!event || event.target !== MESSAGE_TARGET_APP) return + + messageHandler(event.method, event.params, MESSAGE_TARGET_EXTENSION, { + sender + }) + }, [messageHandler]) + + // listen for message + useEffect(() => { + chrome.runtime.onMessage.addListener(extensionMessageCallback) + return () => { + chrome.runtime.onMessage.removeListener(extensionMessageCallback) + } + }, [extensionMessageCallback]) +} + +export default useMessageService diff --git a/src/hooks/useSubtitle.ts b/src/hooks/useSubtitle.ts index 3f589ef..893ecfd 100644 --- a/src/hooks/useSubtitle.ts +++ b/src/hooks/useSubtitle.ts @@ -1,6 +1,8 @@ import {useAppDispatch, useAppSelector} from './redux' import React, {useCallback} from 'react' import {setNeedScroll, setReviewAction, setTempData} from '../redux/envReducer' +import {sendInject} from '../util/biz_util' +import {MESSAGE_TO_INJECT_MOVE} from '../const' const useSubtitle = () => { const dispatch = useAppDispatch() @@ -9,7 +11,7 @@ const useSubtitle = () => { const reviewActions = useAppSelector(state => state.env.tempData.reviewActions) const move = useCallback((time: number, togglePause: boolean) => { - window.parent.postMessage({type: 'move', time, togglePause}, '*') + sendInject(MESSAGE_TO_INJECT_MOVE, {time, togglePause}) //review action if (reviewed === undefined && !reviewAction) { diff --git a/src/hooks/useSubtitleService.ts b/src/hooks/useSubtitleService.ts index 441f516..c65fcae 100644 --- a/src/hooks/useSubtitleService.ts +++ b/src/hooks/useSubtitleService.ts @@ -16,9 +16,11 @@ import { setTempData, } from '../redux/envReducer' import {EventBusContext} from '../Router' -import {EVENT_EXPAND, GEMINI_TOKENS, TOTAL_HEIGHT_MAX, TOTAL_HEIGHT_MIN, WORDS_MIN, WORDS_RATE} from '../const' +import {EVENT_EXPAND, GEMINI_TOKENS, TOTAL_HEIGHT_MAX, TOTAL_HEIGHT_MIN, WORDS_MIN, WORDS_RATE, MESSAGE_TO_INJECT_GET_VIDEO_STATUS, MESSAGE_TO_INJECT_GET_VIDEO_ELEMENT_INFO, MESSAGE_TO_INJECT_REFRESH_VIDEO_INFO, MESSAGE_TO_INJECT_HIDE_TRANS, MESSAGE_TO_INJECT_UPDATETRANSRESULT} from '../const' import {useInterval} from 'ahooks' import {getModelMaxTokens, getWholeText} from '../util/biz_util' +import {sendInject} from '../util/biz_util' +import {MESSAGE_TO_INJECT_GET_SUBTITLE} from '../const' /** * Service是单例,类似后端的服务概念 @@ -52,55 +54,6 @@ const useSubtitleService = () => { } }, [reviewActions, dispatch, reviewed]) - // 监听消息 - useEffect(() => { - const listener = (event: MessageEvent) => { - const data = event.data - - if (data.type === 'setVideoInfo') { - dispatch(setInfos(data.infos)) - dispatch(setUrl(data.url)) - dispatch(setTitle(data.title)) - console.debug('video title: ', data.title) - } - - if (data.type === 'setInfos') { - dispatch(setInfos(data.infos)) - dispatch(setCurInfo(undefined)) - dispatch(setCurFetched(false)) - dispatch(setData(undefined)) - // console.log('setInfos', data.infos) - } - - if (data.type === 'setSubtitle') { - const data_ = data.data.data - data_?.body?.forEach((item: TranscriptItem, idx: number) => { - item.idx = idx - }) - // dispatch(setCurInfo(data.data.info)) - dispatch(setCurFetched(true)) - dispatch(setData(data_)) - // console.log('setSubtitle', data.data) - } - - if (data.type === 'setCurrentTime') { - dispatch(setCurrentTime(data.data.currentTime)) - } - if (data.type === 'setSettings') { - dispatch(setNoVideo(data.data.noVideo)) - if (data.data.totalHeight) { - dispatch(setTotalHeight(Math.min(Math.max(data.data.totalHeight, TOTAL_HEIGHT_MIN), TOTAL_HEIGHT_MAX))) - } - } - } - - window.addEventListener('message', listener) - - return () => { - window.removeEventListener('message', listener) - } - }, [dispatch, eventBus]) - // 有数据时自动展开 useEffect(() => { if ((data != null) && data.body.length > 0) { @@ -120,15 +73,30 @@ const useSubtitleService = () => { // 获取 useEffect(() => { if (curInfo && !curFetched) { - window.parent.postMessage({type: 'getSubtitle', info: curInfo}, '*') + sendInject(MESSAGE_TO_INJECT_GET_SUBTITLE, {info: curInfo}).then(data => { + const data_ = data.data + data_?.body?.forEach((item: TranscriptItem, idx: number) => { + item.idx = idx + }) + // dispatch(setCurInfo(data.data.info)) + dispatch(setCurFetched(true)) + dispatch(setData(data_)) + + console.log('subtitle', data) + }) } }, [curFetched, curInfo]) useEffect(() => { // 初始获取列表 - window.parent.postMessage({type: 'refreshVideoInfo'}, '*') + sendInject(MESSAGE_TO_INJECT_REFRESH_VIDEO_INFO, {}) // 初始获取设置信息 - window.parent.postMessage({type: 'getSettings'}, '*') + sendInject(MESSAGE_TO_INJECT_GET_VIDEO_ELEMENT_INFO, {}).then(info => { + dispatch(setNoVideo(info.noVideo)) + if (info.totalHeight) { + dispatch(setTotalHeight(Math.min(Math.max(info.totalHeight, TOTAL_HEIGHT_MIN), TOTAL_HEIGHT_MAX))) + } + }) }, []) // 更新当前位置 @@ -216,21 +184,23 @@ const useSubtitleService = () => { // 每秒更新当前视频时间 useInterval(() => { - window.parent.postMessage({type: 'getCurrentTime'}, '*') + sendInject(MESSAGE_TO_INJECT_GET_VIDEO_STATUS, {}).then(status => { + dispatch(setCurrentTime(status.currentTime)) + }) }, 500) // show translated text in the video useEffect(() => { if (hideOnDisableAutoTranslate && !autoTranslate) { - window.parent.postMessage({type: 'updateTransResult'}, '*') + sendInject(MESSAGE_TO_INJECT_HIDE_TRANS, {}) return } const transResult = curIdx?transResults[curIdx]:undefined if (transResult?.code === '200' && transResult.data) { - window.parent.postMessage({type: 'updateTransResult', result: transResult.data}, '*') + sendInject(MESSAGE_TO_INJECT_UPDATETRANSRESULT, {result: transResult.data}) } else { - window.parent.postMessage({type: 'updateTransResult'}, '*') + sendInject(MESSAGE_TO_INJECT_HIDE_TRANS, {}) } }, [autoTranslate, curIdx, hideOnDisableAutoTranslate, transResults]) } diff --git a/src/hooks/useTranslate.ts b/src/hooks/useTranslate.ts index d622ea0..89b84dd 100644 --- a/src/hooks/useTranslate.ts +++ b/src/hooks/useTranslate.ts @@ -16,6 +16,8 @@ import { import { LANGUAGE_DEFAULT, LANGUAGES_MAP, + MESSAGE_TO_EXTENSION_ADD_TASK, + MESSAGE_TO_EXTENSION_GET_TASK, PROMPT_DEFAULTS, PROMPT_TYPE_ASK, PROMPT_TYPE_TRANSLATE, @@ -27,7 +29,7 @@ import { } from '../const' import toast from 'react-hot-toast' import {useMemoizedFn} from 'ahooks/es' -import {extractJsonArray, extractJsonObject, getModel} from '../util/biz_util' +import {extractJsonArray, extractJsonObject, getModel, sendExtension} from '../util/biz_util' import {formatTime} from '../util/util' const useTranslate = () => { @@ -135,7 +137,7 @@ const useTranslate = () => { } }) dispatch(addTransResults(result)) - const task = await chrome.runtime.sendMessage({type: 'addTask', taskDef}) + const task = await sendExtension(MESSAGE_TO_EXTENSION_ADD_TASK, {taskDef}) dispatch(addTaskId(task.id)) } } @@ -205,7 +207,7 @@ const useTranslate = () => { console.debug('addSummarizeTask', taskDef) dispatch(setSummaryStatus({segmentStartIdx: segment.startIdx, type, status: 'pending'})) dispatch(setLastSummarizeTime(Date.now())) - const task = await chrome.runtime.sendMessage({type: 'addTask', taskDef}) + const task = await sendExtension(MESSAGE_TO_EXTENSION_ADD_TASK, {taskDef}) dispatch(addTaskId(task.id)) } }, [dispatch, envData, summarizeLanguage.name, title]) @@ -262,7 +264,7 @@ const useTranslate = () => { id, status: 'pending' })) - const task = await chrome.runtime.sendMessage({type: 'addTask', taskDef}) + const task = await sendExtension(MESSAGE_TO_EXTENSION_ADD_TASK, {taskDef}) dispatch(addTaskId(task.id)) } }, [dispatch, envData, summarizeLanguage.name, title]) @@ -330,7 +332,7 @@ const useTranslate = () => { }) const getTask = useCallback(async (taskId: string) => { - const taskResp = await chrome.runtime.sendMessage({type: 'getTask', taskId}) + const taskResp = await sendExtension(MESSAGE_TO_EXTENSION_GET_TASK, {taskId}) if (taskResp.code === 'ok') { console.debug('getTask', taskResp.task) const task: Task = taskResp.task diff --git a/src/inject/inject.ts b/src/inject/inject.ts index 8d34f51..4dee7db 100644 --- a/src/inject/inject.ts +++ b/src/inject/inject.ts @@ -1,264 +1,402 @@ -import {TOTAL_HEIGHT_DEF, HEADER_HEIGHT, TOTAL_HEIGHT_MIN, TOTAL_HEIGHT_MAX, IFRAME_ID} from '@/const' -let totalHeight = TOTAL_HEIGHT_DEF +import { TOTAL_HEIGHT_DEF, HEADER_HEIGHT, TOTAL_HEIGHT_MIN, TOTAL_HEIGHT_MAX, IFRAME_ID, MESSAGE_TO_INJECT_DOWNLOAD_AUDIO, MESSAGE_TARGET_INJECT, MESSAGE_TO_APP_SET_INFOS } from '@/const' +import { PostMessagePayload, PostMessageResponse, startListening } from 'postmessage-promise' +import {MESSAGE_TARGET_EXTENSION, MESSAGE_TO_INJECT_FOLD, MESSAGE_TO_INJECT_MOVE, MESSAGE_TO_APP_SET_VIDEO_INFO, MESSAGE_TO_INJECT_GET_SUBTITLE, MESSAGE_TO_INJECT_GET_VIDEO_STATUS, MESSAGE_TO_INJECT_GET_VIDEO_ELEMENT_INFO, MESSAGE_TO_INJECT_UPDATETRANSRESULT, MESSAGE_TO_INJECT_PLAY, MESSAGE_TO_INJECT_HIDE_TRANS, MESSAGE_TO_INJECT_REFRESH_VIDEO_INFO} from '@/const' -const getVideoElement = () => { - const videoWrapper = document.getElementById('bilibili-player') - return videoWrapper?.querySelector('video') as HTMLVideoElement | undefined +const debug = (...args: any[]) => { + console.debug('[Inject]', ...args) } -const timerIframe = setInterval(function () { - var danmukuBox = document.getElementById('danmukuBox') - if (danmukuBox) { - clearInterval(timerIframe) +(function () { + const runtime: { + postMessageToApp?: (method: string, payload: PostMessagePayload) => Promise + // lastV?: string | null + // lastVideoInfo?: VideoInfo - //延迟插入iframe(插入太快,网络较差时容易出现b站网页刷新,原因暂时未知,可能b站的某种机制?) - setTimeout(() => { - var vKey = '' - for (const key in danmukuBox?.dataset) { - if (key.startsWith('v-')) { - vKey = key - break + fold: boolean + + videoElement?: HTMLVideoElement + videoElementHeight: number + + showTrans: boolean + curTrans?: string + } = { + fold: true, + videoElementHeight: TOTAL_HEIGHT_DEF, + showTrans: false, + } + + const sendExtension = async (method: string, params?: any) => { + return await chrome.runtime.sendMessage({ + target: MESSAGE_TARGET_EXTENSION, + method, + params: params??{}, + }).then((messageResult) => { + if (messageResult.success) { + return messageResult.data as T + } else { + throw new Error(messageResult.message) + } + }) + } + + const sendApp = async (method: string, params: any) => { + if (runtime.postMessageToApp != null) { + const messageResult = await runtime.postMessageToApp(method, params) as MessageResult | undefined + if (messageResult != null) { + if (messageResult.success) { + return messageResult.data as T + } else { + throw new Error(messageResult.message) + } + } else { + throw new Error('no response') + } + } + } + + const getVideoElement = () => { + const videoWrapper = document.getElementById('bilibili-player') + return videoWrapper?.querySelector('video') as HTMLVideoElement | undefined + } + + /** + * @return if changed + */ + const refreshVideoElement = () => { + const newVideoElement = getVideoElement() + const newVideoElementHeight = (newVideoElement != null)?(Math.min(Math.max(newVideoElement.offsetHeight, TOTAL_HEIGHT_MIN), TOTAL_HEIGHT_MAX)):TOTAL_HEIGHT_DEF + if (newVideoElement === runtime.videoElement && Math.abs(newVideoElementHeight - runtime.videoElementHeight) < 1) { + return false + } else { + runtime.videoElement = newVideoElement + runtime.videoElementHeight = newVideoElementHeight + // update iframe height + updateIframeHeight() + return true + } + } + + const timerIframe = setInterval(function () { + var danmukuBox = document.getElementById('danmukuBox') + if (danmukuBox) { + clearInterval(timerIframe) + + //延迟插入iframe(插入太快,网络较差时容易出现b站网页刷新,原因暂时未知,可能b站的某种机制?) + setTimeout(() => { + var vKey = '' + for (const key in danmukuBox?.dataset) { + if (key.startsWith('v-')) { + vKey = key + break + } + } + + const iframe = document.createElement('iframe') + iframe.id = IFRAME_ID + iframe.src = chrome.runtime.getURL('index.html') + iframe.style.border = 'none' + iframe.style.width = '100%' + iframe.style.height = '44px' + iframe.style.marginBottom = '3px' + iframe.allow = 'clipboard-read; clipboard-write;' + if (vKey) { + iframe.dataset[vKey] = danmukuBox?.dataset[vKey] + } + //insert before first child + danmukuBox?.insertBefore(iframe, danmukuBox?.firstChild) + + debug('iframe inserted') + }, 1500) + } + }, 1000) + + let aid: number | null = null + let title = '' + let pages: any[] = [] + let pagesMap: Record = {} + + let lastAidOrBvid: string | null = null + const refreshVideoInfo = async () => { + const iframe = document.getElementById(IFRAME_ID) as HTMLIFrameElement | undefined + if (!iframe) return + + // fix: https://github.com/IndieKKY/bilibili-subtitle/issues/5 + // 处理稍后再看的url( https://www.bilibili.com/list/watchlater?bvid=xxx&oid=xxx ) + const pathSearchs: Record = {} + location.search.slice(1).replace(/([^=&]*)=([^=&]*)/g, (matchs, a, b, c) => pathSearchs[a] = b) + + // bvid + let aidOrBvid = pathSearchs.bvid // 默认为稍后再看 + if (!aidOrBvid) { + let path = location.pathname + if (path.endsWith('/')) { + path = path.slice(0, -1) + } + const paths = path.split('/') + aidOrBvid = paths[paths.length - 1] + } + + if (aidOrBvid !== lastAidOrBvid) { + // console.debug('refreshVideoInfo') + + lastAidOrBvid = aidOrBvid + if (aidOrBvid) { + //aid,pages + let cid + let subtitles + if (aidOrBvid.toLowerCase().startsWith('av')) {//avxxx + aid = parseInt(aidOrBvid.slice(2)) + pages = await fetch(`https://api.bilibili.com/x/player/pagelist?aid=${aid}`, { credentials: 'include' }).then(res => res.json()).then(res => res.data) + cid = pages[0].cid + title = pages[0].part + await fetch(`https://api.bilibili.com/x/player/v2?aid=${aid}&cid=${cid}`, { credentials: 'include' }).then(res => res.json()).then(res => { + subtitles = res.data.subtitle.subtitles + }) + } else {//bvxxx + await fetch(`https://api.bilibili.com/x/web-interface/view?bvid=${aidOrBvid}`, { credentials: 'include' }).then(res => res.json()).then(async res => { + title = res.data.title + aid = res.data.aid + cid = res.data.cid + pages = res.data.pages + }) + await fetch(`https://api.bilibili.com/x/player/v2?aid=${aid}&cid=${cid}`, { credentials: 'include' }).then(res => res.json()).then(res => { + subtitles = res.data.subtitle.subtitles + }) + } + + //pagesMap + pagesMap = {} + pages.forEach(page => { + pagesMap[page.page + ''] = page + }) + + debug('refreshVideoInfo: ', aid, cid, pages, subtitles) + + //send setVideoInfo + sendApp(MESSAGE_TO_APP_SET_VIDEO_INFO, { + url: location.origin + location.pathname, + title, + aid, + pages, + infos: subtitles, + }) + } + } + } + + let lastAid: number | null = null + let lastCid: number | null = null + const refreshSubtitles = () => { + const iframe = document.getElementById(IFRAME_ID) as HTMLIFrameElement | undefined + if (!iframe) return + + const urlSearchParams = new URLSearchParams(window.location.search) + const p = urlSearchParams.get('p') || 1 + const page = pagesMap[p] + if (!page) return + const cid = page.cid + + if (aid !== lastAid || cid !== lastCid) { + debug('refreshSubtitles', aid, cid) + + lastAid = aid + lastCid = cid + if (aid && cid) { + fetch(`https://api.bilibili.com/x/player/v2?aid=${aid}&cid=${cid}`, { + credentials: 'include', + }) + .then(res => res.json()) + .then(res => { + // console.log('refreshSubtitles: ', aid, cid, res) + sendApp(MESSAGE_TO_APP_SET_INFOS, { + infos: res.data.subtitle.subtitles + }) + }) + } + } + } + + const updateIframeHeight = () => { + const iframe = document.getElementById(IFRAME_ID) as HTMLIFrameElement | undefined + if (iframe != null) { + iframe.style.height = (runtime.fold ? HEADER_HEIGHT : runtime.videoElementHeight) + 'px' + } + } + + const methods: { + [key: string]: (params: any, context: MethodContext) => Promise + } = { + [MESSAGE_TO_INJECT_FOLD]: async (params) => { + runtime.fold = params.fold + updateIframeHeight() + }, + [MESSAGE_TO_INJECT_MOVE]: async (params) => { + const video = getVideoElement() + if (video != null) { + video.currentTime = params.time + if (params.togglePause) { + video.paused ? video.play() : video.pause() } } - - const iframe = document.createElement('iframe') - iframe.id = IFRAME_ID - iframe.src = chrome.runtime.getURL('index.html') - iframe.style.border = 'none' - iframe.style.width = '100%' - iframe.style.height = '44px' - iframe.style.marginBottom = '3px' - iframe.allow = 'clipboard-read; clipboard-write;' - if (vKey) { - iframe.dataset[vKey] = danmukuBox?.dataset[vKey] + }, + [MESSAGE_TO_INJECT_GET_SUBTITLE]: async (params) => { + let url = params.info.subtitle_url + if (url.startsWith('http://')) { + url = url.replace('http://', 'https://') } - //insert before first child - danmukuBox?.insertBefore(iframe, danmukuBox?.firstChild) - - console.debug('iframe inserted') - }, 1500) - } -}, 1000) - -let aid: number | null = null -let title = '' -let pages: any[] = [] -let pagesMap: Record = {} - -let lastAidOrBvid: string | null = null -const refreshVideoInfo = async () => { - const iframe = document.getElementById(IFRAME_ID) as HTMLIFrameElement | undefined - if (!iframe) return - - // fix: https://github.com/IndieKKY/bilibili-subtitle/issues/5 - // 处理稍后再看的url( https://www.bilibili.com/list/watchlater?bvid=xxx&oid=xxx ) - const pathSearchs: Record = {} - location.search.slice(1).replace(/([^=&]*)=([^=&]*)/g, (matchs, a, b, c) => pathSearchs[a] = b) - - // bvid - let aidOrBvid = pathSearchs.bvid // 默认为稍后再看 - if (!aidOrBvid) { - let path = location.pathname - if (path.endsWith('/')) { - path = path.slice(0, -1) - } - const paths = path.split('/') - aidOrBvid = paths[paths.length - 1] - } - - if (aidOrBvid !== lastAidOrBvid) { - // console.debug('refreshVideoInfo') - - lastAidOrBvid = aidOrBvid - if (aidOrBvid) { - //aid,pages - let cid - let subtitles - if (aidOrBvid.toLowerCase().startsWith('av')) {//avxxx - aid = parseInt(aidOrBvid.slice(2)) - pages = await fetch(`https://api.bilibili.com/x/player/pagelist?aid=${aid}`, {credentials: 'include'}).then(res => res.json()).then(res => res.data) - cid = pages[0].cid - title = pages[0].part - await fetch(`https://api.bilibili.com/x/player/v2?aid=${aid}&cid=${cid}`, {credentials: 'include'}).then(res => res.json()).then(res => { - subtitles = res.data.subtitle.subtitles - }) - } else {//bvxxx - await fetch(`https://api.bilibili.com/x/web-interface/view?bvid=${aidOrBvid}`, {credentials: 'include'}).then(res => res.json()).then(async res => { - title = res.data.title - aid = res.data.aid - cid = res.data.cid - pages = res.data.pages - }) - await fetch(`https://api.bilibili.com/x/player/v2?aid=${aid}&cid=${cid}`, {credentials: 'include'}).then(res => res.json()).then(res => { - subtitles = res.data.subtitle.subtitles - }) - } - - //pagesMap - pagesMap = {} - pages.forEach(page => { - pagesMap[page.page + ''] = page - }) - - console.debug('refreshVideoInfo: ', aid, cid, pages, subtitles) - - //send setVideoInfo - iframe.contentWindow?.postMessage({ - type: 'setVideoInfo', - url: location.origin + location.pathname, - title, - aid, - pages, - infos: subtitles, - }, '*') - } - } -} - -let lastAid: number | null = null -let lastCid: number | null = null -const refreshSubtitles = () => { - const iframe = document.getElementById(IFRAME_ID) as HTMLIFrameElement | undefined - if (!iframe) return - - const urlSearchParams = new URLSearchParams(window.location.search) - const p = urlSearchParams.get('p') || 1 - const page = pagesMap[p] - if (!page) return - const cid = page.cid - - if (aid !== lastAid || cid !== lastCid) { - console.debug('refreshSubtitles', aid, cid) - - lastAid = aid - lastCid = cid - if (aid && cid) { - fetch(`https://api.bilibili.com/x/player/v2?aid=${aid}&cid=${cid}`, { - credentials: 'include', - }) - .then(res => res.json()) - .then(res => { - // console.log('refreshSubtitles: ', aid, cid, res) - iframe.contentWindow?.postMessage({ - type: 'setInfos', - infos: res.data.subtitle.subtitles - }, '*') - }) - } - } -} - -// 监听消息 -window.addEventListener("message", (event) => { - const {data} = event - - if (data.type === 'fold') { - const iframe = document.getElementById(IFRAME_ID) as HTMLIFrameElement | undefined - if (iframe) { - iframe.style.height = (data.fold ? HEADER_HEIGHT : totalHeight) + 'px' - } - } - - if (data.type === 'move') { - const video = getVideoElement() - if (video) { - video.currentTime = data.time - if (data.togglePause) { - video.paused ? video.play() : video.pause() - } - } - } - - //刷新视频信息 - if (data.type === 'refreshVideoInfo') { - refreshVideoInfo().catch(console.error) - } - //刷新字幕 - if (data.type === 'refreshSubtitles') { - refreshSubtitles() - } - if (data.type === 'getSubtitle') { - let url = data.info.subtitle_url - if (url.startsWith('http://')) { - url = url.replace('http://', 'https://') - } - fetch(url).then(res => res.json()).then(res => { - event.source?.postMessage({ - data: { - info: data.info, - data: res, - }, type: 'setSubtitle' - // @ts-ignore - }, '*') - }) - } - - if (data.type === 'getCurrentTime') { - const video = getVideoElement() - if (video) { - event.source?.postMessage({ - data: { + return await fetch(url).then(res => res.json()) + }, + [MESSAGE_TO_INJECT_GET_VIDEO_STATUS]: async (params) => { + const video = getVideoElement() + if (video != null) { + return { + paused: video.paused, currentTime: video.currentTime - }, type: 'setCurrentTime' - // @ts-ignore - }, '*') - } - } - - if (data.type === 'getSettings') { - const videoElement = getVideoElement() - totalHeight = videoElement ? Math.min(Math.max(videoElement.offsetHeight, TOTAL_HEIGHT_MIN), TOTAL_HEIGHT_MAX) : TOTAL_HEIGHT_DEF - event.source?.postMessage({ - data: { - noVideo: !videoElement, - totalHeight, - }, type: 'setSettings' - // @ts-ignore - }, '*') - } - - if (data.type === 'downloadAudio') { - const html = document.getElementsByTagName('html')[0].innerHTML - const playInfo = JSON.parse(html.match(/window.__playinfo__=(.+?)<\/script/)?.[1] ?? '{}') - const audioUrl = playInfo.data.dash.audio[0].baseUrl - - fetch(audioUrl).then(res => res.blob()).then(blob => { - const a = document.createElement('a') - a.href = URL.createObjectURL(blob) - a.download = `${title}.m4s` - a.click() - }) - } - - if (data.type === 'updateTransResult') { - const trans = data.result??'' - let text = document.getElementById('trans-result-text') - if (text) { - text.innerHTML = trans - } else { - const container = document.getElementsByClassName('bpx-player-subtitle-panel-wrap')?.[0] - if (container) { - const div = document.createElement('div') - div.style.display = 'flex' - div.style.justifyContent = 'center' - div.style.margin = '2px' - text = document.createElement('text') - text.id = 'trans-result-text' - text.innerHTML = trans - text.style.fontSize = '1rem' - text.style.padding = '5px' - text.style.color = 'white' - text.style.background = 'rgba(0, 0, 0, 0.4)' - div.append(text) - - container.append(div) + } } - } - text && (text.style.display = trans ? 'block' : 'none') - } -}, false); + }, + [MESSAGE_TO_INJECT_GET_VIDEO_ELEMENT_INFO]: async (params) => { + refreshVideoElement() + return { + noVideo: runtime.videoElement == null, + totalHeight: runtime.videoElementHeight, + } + }, + [MESSAGE_TO_INJECT_REFRESH_VIDEO_INFO]: async (params) => { + refreshVideoInfo() + }, + [MESSAGE_TO_INJECT_UPDATETRANSRESULT]: async (params) => { + runtime.showTrans = true + runtime.curTrans = params?.result -setInterval(() => { - refreshVideoInfo().catch(console.error) - refreshSubtitles() -}, 1000) + let text = document.getElementById('trans-result-text') + if (text) { + text.innerHTML = runtime.curTrans??'' + } else { + const container = document.getElementsByClassName('bpx-player-subtitle-panel-wrap')?.[0] + if (container) { + const div = document.createElement('div') + div.style.display = 'flex' + div.style.justifyContent = 'center' + div.style.margin = '2px' + text = document.createElement('text') + text.id = 'trans-result-text' + text.innerHTML = runtime.curTrans??'' + text.style.fontSize = '1rem' + text.style.padding = '5px' + text.style.color = 'white' + text.style.background = 'rgba(0, 0, 0, 0.4)' + div.append(text) + + container.append(div) + } + } + text && (text.style.display = runtime.curTrans ? 'block' : 'none') + }, + [MESSAGE_TO_INJECT_HIDE_TRANS]: async (params) => { + runtime.showTrans = false + runtime.curTrans = undefined + + let text = document.getElementById('trans-result-text') + if (text) { + text.style.display = 'none' + } + }, + [MESSAGE_TO_INJECT_PLAY]: async (params) => { + const { play } = params + const video = getVideoElement() + if (video != null) { + if (play) { + await video.play() + } else { + video.pause() + } + } + }, + [MESSAGE_TO_INJECT_DOWNLOAD_AUDIO]: async (params) => { + const html = document.getElementsByTagName('html')[0].innerHTML + const playInfo = JSON.parse(html.match(/window.__playinfo__=(.+?)<\/script/)?.[1] ?? '{}') + const audioUrl = playInfo.data.dash.audio[0].baseUrl + + fetch(audioUrl).then(res => res.blob()).then(blob => { + const a = document.createElement('a') + a.href = URL.createObjectURL(blob) + a.download = `${title}.m4s` + a.click() + }) + }, + } + + /** + * @param sendResponse No matter what is returned, this method will definitely be called. + */ + const messageHandler = (event: MessageData, sender: chrome.runtime.MessageSender | null, sendResponse: (response?: MessageResult) => void) => { + const source = sender != null?((sender.tab != null) ? `tab ${sender.tab.url ?? ''}` : 'extension'):'app' + debug(`${source} => `, JSON.stringify(event)) + + // check event target + if (event.target !== MESSAGE_TARGET_INJECT) return + + const method = methods[event.method] + if (method != null) { + method(event.params, { + event, + sender, + }).then(data => { + // debug(`${source} <= `, event.method, JSON.stringify(data)) + return data + }).then(data => sendResponse({ + success: true, + code: 200, + data, + })).catch(err => { + console.error(err) + let message + if (err instanceof Error) { + message = err.message + } else if (typeof err === 'string') { + message = err + } else { + message = 'error: ' + JSON.stringify(err) + } + sendResponse({ + success: false, + code: 500, + message, + }) + }) + return true + } else { + console.error('Unknown method:', event.method) + sendResponse({ + success: false, + code: 501, + message: 'Unknown method: ' + event.method, + }) + } + } + + // listen message from app + startListening({}).then(e => { + const { postMessage, listenMessage, destroy } = e + runtime.postMessageToApp = postMessage + listenMessage((method, params, sendResponse) => { + messageHandler({ + target: MESSAGE_TARGET_INJECT, + method, + params, + }, null, sendResponse) + }) + }).catch(console.error) + + /** + * listen message from extension + * Attention: return true if you need to sendResponse asynchronously + */ + chrome.runtime.onMessage.addListener(messageHandler) + + setInterval(() => { + refreshVideoInfo().catch(console.error) + refreshSubtitles() + }, 1000) +})() diff --git a/src/typings.d.ts b/src/typings.d.ts index 8bcee11..320162e 100644 --- a/src/typings.d.ts +++ b/src/typings.d.ts @@ -1,3 +1,23 @@ + +interface MessageData { + target: string + method: string + params?: any + [key: string]: any +} + +interface MessageResult { + success: boolean + code: number + message?: string + data?: any +} + +interface MethodContext { + event: any + sender?: chrome.runtime.MessageSender | null +} + interface EnvData { autoExpand?: boolean flagDot?: boolean diff --git a/src/util/biz_util.ts b/src/util/biz_util.ts index e4ba29a..2982b15 100644 --- a/src/util/biz_util.ts +++ b/src/util/biz_util.ts @@ -2,6 +2,42 @@ import {APP_DOM_ID, CUSTOM_MODEL_TOKENS, MODEL_DEFAULT, MODEL_MAP, SUMMARIZE_TYP import {isDarkMode} from '@kky002/kky-util' import toast from 'react-hot-toast' import {findIndex} from 'lodash-es' +import {MESSAGE_TARGET_EXTENSION} from '../const' +import {injectWaiter} from '../hooks/useMessageService' + +export const debug = (...args: any[]) => { + console.debug('[APP]', ...args) +} + +export const sendExtension = async (method: string, params?: any) => { + return await chrome.runtime.sendMessage({ + target: MESSAGE_TARGET_EXTENSION, + method, + params: params??{}, + }).then((messageResult) => { + if (messageResult.success) { + return messageResult.data as T + } else { + throw new Error(messageResult.message) + } + }) +} + +export const sendInject = async (method: string, params?: any) => { + // wait + const postInjectMessage = await injectWaiter.wait() + // send message + const messageResult = await postInjectMessage(method, params) as MessageResult | undefined + if (messageResult != null) { + if (messageResult.success) { + return messageResult.data as T + } else { + throw new Error(messageResult.message) + } + } else { + throw new Error('no response') + } +} /** * 获取译文