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')
+ }
+}
/**
* 获取译文