重构消息通信

This commit is contained in:
IndieKKY
2024-10-03 23:38:18 +08:00
parent e3ddf386cb
commit f50a2e3abd
13 changed files with 719 additions and 374 deletions

View File

@@ -16,7 +16,7 @@
"@crxjs/vite-plugin": "^1.0.14", "@crxjs/vite-plugin": "^1.0.14",
"@kky002/kky-hooks": "^1.2.1", "@kky002/kky-hooks": "^1.2.1",
"@kky002/kky-ui": "^1.0.9", "@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", "@logto/react": "1.0.0-beta.13",
"@popperjs/core": "^2.11.6", "@popperjs/core": "^2.11.6",
"@reduxjs/toolkit": "^1.8.5", "@reduxjs/toolkit": "^1.8.5",
@@ -28,6 +28,7 @@
"less": "^4.1.3", "less": "^4.1.3",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"pako": "^2.1.0", "pako": "^2.1.0",
"postmessage-promise": "^3.2.1",
"qs": "^6.11.0", "qs": "^6.11.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
@@ -46,10 +47,10 @@
"devDependencies": { "devDependencies": {
"@tailwindcss/line-clamp": "^0.4.2", "@tailwindcss/line-clamp": "^0.4.2",
"@tailwindcss/typography": "^0.5.8", "@tailwindcss/typography": "^0.5.8",
"@types/node": "^20.8.10",
"@types/chrome": "^0.0.203", "@types/chrome": "^0.0.203",
"@types/js-search": "^1.4.0", "@types/js-search": "^1.4.0",
"@types/lodash-es": "^4.17.6", "@types/lodash-es": "^4.17.6",
"@types/node": "^20.8.10",
"@types/pako": "^2.0.0", "@types/pako": "^2.0.0",
"@types/qs": "^6.9.7", "@types/qs": "^6.9.7",
"@types/react": "^18.0.20", "@types/react": "^18.0.20",

29
pnpm-lock.yaml generated
View File

@@ -18,8 +18,8 @@ importers:
specifier: ^1.0.9 specifier: ^1.0.9
version: 1.0.9 version: 1.0.9
'@kky002/kky-util': '@kky002/kky-util':
specifier: ^1.4.2 specifier: ^1.13.13
version: 1.4.2 version: 1.13.13
'@logto/react': '@logto/react':
specifier: 1.0.0-beta.13 specifier: 1.0.0-beta.13
version: 1.0.0-beta.13(react@18.2.0) version: 1.0.0-beta.13(react@18.2.0)
@@ -53,6 +53,9 @@ importers:
pako: pako:
specifier: ^2.1.0 specifier: ^2.1.0
version: 2.1.0 version: 2.1.0
postmessage-promise:
specifier: ^3.2.1
version: 3.2.1
qs: qs:
specifier: ^6.11.0 specifier: ^6.11.0
version: 6.11.0 version: 6.11.0
@@ -390,8 +393,8 @@ packages:
'@kky002/kky-ui@1.0.9': '@kky002/kky-ui@1.0.9':
resolution: {integrity: sha512-pepfRcLfC1eIQ1lsSJLWNr4PgdLqFLuvQMlitJy7W668yZ7qu8yAHSjg8A20R7HB4mFkJ+B96WETalOar1e/kA==} resolution: {integrity: sha512-pepfRcLfC1eIQ1lsSJLWNr4PgdLqFLuvQMlitJy7W668yZ7qu8yAHSjg8A20R7HB4mFkJ+B96WETalOar1e/kA==}
'@kky002/kky-util@1.4.2': '@kky002/kky-util@1.13.13':
resolution: {integrity: sha512-gpZHWuCBBgYV1rnZ07FhriCR7x2228LOnf6PI6nyfWXxYYy1RQ8MZcdegbBsi/HRmh7EsW1yPqq85pwNLX0B9w==} resolution: {integrity: sha512-DvePr8J7dyOaVteU/bskuoL3noHiOKpX3IGhN1h0v/Nt/fGI/tA1JKwUVrg4PE89xKAhH7c2Z+RHLa8zj7w7ng==}
'@logto/browser@1.0.0-beta.13': '@logto/browser@1.0.0-beta.13':
resolution: {integrity: sha512-ddAVggFcbS9yfG8Gvn2xknE2NZd6+lGxOQ6UbjIJKsYBAsJG95u1ITYaP7tNSDdxqZPmSBGXp4rfsQB+u0JPJQ==} resolution: {integrity: sha512-ddAVggFcbS9yfG8Gvn2xknE2NZd6+lGxOQ6UbjIJKsYBAsJG95u1ITYaP7tNSDdxqZPmSBGXp4rfsQB+u0JPJQ==}
@@ -2005,6 +2008,9 @@ packages:
resolution: {integrity: sha512-h+pbPsyhlYj6N2ozBmHhHrs9DzGmbaarbLvWipMRO7RLS+v4onj26MPFXA5OBYFxyqYhUJK456SwDcY9H2/zsA==} resolution: {integrity: sha512-h+pbPsyhlYj6N2ozBmHhHrs9DzGmbaarbLvWipMRO7RLS+v4onj26MPFXA5OBYFxyqYhUJK456SwDcY9H2/zsA==}
engines: {node: ^10 || ^12 || >=14} engines: {node: ^10 || ^12 || >=14}
postmessage-promise@3.2.1:
resolution: {integrity: sha512-cSs5eg+DvBQIdIQK9Cimd1wB2eb85xlzJXkJwm6jYNcTlsiwTFXvdyF/69JFozX6vIkdYz2Jv31W+BvSKQXNVg==}
prelude-ls@1.2.1: prelude-ls@1.2.1:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}
@@ -2384,6 +2390,10 @@ packages:
engines: {node: '>=4.2.0'} engines: {node: '>=4.2.0'}
hasBin: true hasBin: true
ua-parser-js@1.0.39:
resolution: {integrity: sha512-k24RCVWlEcjkdOxYmVJgeD/0a1TiSpqLg+ZalVGV9lsnr4yqu0w7tX/x2xX6G4zpkgQnRf89lxuZ1wsbjXM8lw==}
hasBin: true
uberproto@1.2.0: uberproto@1.2.0:
resolution: {integrity: sha512-pGtPAQmLwh+R9w81WVHzui1FfedpQWQpiaIIfPCwhtsBez4q6DYbJFfyXPVHPUTNFnedAvNEnkoFiLuhXIR94w==} resolution: {integrity: sha512-pGtPAQmLwh+R9w81WVHzui1FfedpQWQpiaIIfPCwhtsBez4q6DYbJFfyXPVHPUTNFnedAvNEnkoFiLuhXIR94w==}
@@ -2804,7 +2814,7 @@ snapshots:
'@kky002/kky-hooks@1.2.1': '@kky002/kky-hooks@1.2.1':
dependencies: dependencies:
'@kky002/kky-util': 1.4.2 '@kky002/kky-util': 1.13.13
ahooks: 3.7.5(react@18.2.0) ahooks: 3.7.5(react@18.2.0)
lodash-es: 4.17.21 lodash-es: 4.17.21
react: 18.2.0 react: 18.2.0
@@ -2813,10 +2823,11 @@ snapshots:
dependencies: dependencies:
react: 18.2.0 react: 18.2.0
'@kky002/kky-util@1.4.2': '@kky002/kky-util@1.13.13':
dependencies: dependencies:
lodash-es: 4.17.21 lodash-es: 4.17.21
qs: 6.11.0 qs: 6.11.0
ua-parser-js: 1.0.39
'@logto/browser@1.0.0-beta.13': '@logto/browser@1.0.0-beta.13':
dependencies: dependencies:
@@ -4657,6 +4668,10 @@ snapshots:
picocolors: 1.0.0 picocolors: 1.0.0
source-map-js: 1.0.2 source-map-js: 1.0.2
postmessage-promise@3.2.1:
dependencies:
'@babel/runtime': 7.19.0
prelude-ls@1.2.1: {} prelude-ls@1.2.1: {}
prop-types@15.8.1: prop-types@15.8.1:
@@ -5045,6 +5060,8 @@ snapshots:
typescript@4.8.3: {} typescript@4.8.3: {}
ua-parser-js@1.0.39: {}
uberproto@1.2.0: {} uberproto@1.2.0: {}
unbox-primitive@1.0.2: unbox-primitive@1.0.2:

View File

@@ -6,16 +6,17 @@ import Header from './biz/Header'
import Body from './biz/Body' import Body from './biz/Body'
import useSubtitleService from './hooks/useSubtitleService' import useSubtitleService from './hooks/useSubtitleService'
import {cloneDeep} from 'lodash-es' 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 {EventBusContext} from './Router'
import useTranslateService from './hooks/useTranslateService' import useTranslateService from './hooks/useTranslateService'
import Settings from './biz/Settings' import Settings from './biz/Settings'
import classNames from 'classnames'
import {handleJson} from '@kky002/kky-util' import {handleJson} from '@kky002/kky-util'
import {useLocalStorage} from '@kky002/kky-hooks' import {useLocalStorage} from '@kky002/kky-hooks'
import {Toaster} from 'react-hot-toast' import {Toaster} from 'react-hot-toast'
import {setTheme} from './util/biz_util' import {setTheme} from './util/biz_util'
import {sendInject} from './util/biz_util'
import useSearchService from './hooks/useSearchService' import useSearchService from './hooks/useSearchService'
import useMessageService from './hooks/useMessageService'
function App() { function App() {
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
@@ -29,7 +30,7 @@ function App() {
const foldCallback = useCallback(() => { const foldCallback = useCallback(() => {
dispatch(setFold(!fold)) dispatch(setFold(!fold))
dispatch(setPage(PAGE_MAIN)) dispatch(setPage(PAGE_MAIN))
window.parent.postMessage({type: 'fold', fold: !fold}, '*') sendInject(MESSAGE_TO_INJECT_FOLD, {fold: !fold})
}, [dispatch, fold]) }, [dispatch, fold])
// handle event // handle event
@@ -74,6 +75,7 @@ function App() {
useSubtitleService() useSubtitleService()
useTranslateService() useTranslateService()
useSearchService() useSearchService()
useMessageService()
return <div className='select-none w-full' style={{ return <div className='select-none w-full' style={{
height: fold?undefined:`${totalHeight}px`, height: fold?undefined:`${totalHeight}px`,

View File

@@ -13,11 +13,11 @@ import {Placement} from '@popperjs/core/lib/enums'
import {useAppDispatch, useAppSelector} from '../hooks/redux' import {useAppDispatch, useAppSelector} from '../hooks/redux'
import {setEnvData, setPage, setTempData} from '../redux/envReducer' import {setEnvData, setPage, setTempData} from '../redux/envReducer'
import {EventBusContext} from '../Router' import {EventBusContext} from '../Router'
import {EVENT_EXPAND, PAGE_SETTINGS} from '../const' import {EVENT_EXPAND, MESSAGE_TO_INJECT_DOWNLOAD_AUDIO, PAGE_SETTINGS} from '../const'
import {formatSrtTime, formatTime, formatVttTime} from '../util/util' import {formatSrtTime, formatTime, formatVttTime} from '../util/util'
import {downloadText, openUrl} from '@kky002/kky-util' import {downloadText, openUrl} from '@kky002/kky-util'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import {getSummarize} from '../util/biz_util' import {getSummarize, sendInject} from '../util/biz_util'
interface Props { interface Props {
placement: Placement placement: Placement
@@ -160,9 +160,7 @@ const MoreBtn = (props: Props) => {
}, [curSummaryType, data, downloadType, segments, title, url]) }, [curSummaryType, data, downloadType, segments, title, url])
const downloadAudioCallback = useCallback(() => { const downloadAudioCallback = useCallback(() => {
window.parent.postMessage({ sendInject(MESSAGE_TO_INJECT_DOWNLOAD_AUDIO, {})
type: 'downloadAudio',
}, '*')
}, []) }, [])
const selectCallback = useCallback((e: any) => { const selectCallback = useCallback((e: any) => {

View File

@@ -1,59 +1,37 @@
import {v4} from 'uuid' import {v4} from 'uuid'
import {handleTask, initTaskService, tasksMap} from './taskService' import {handleTask, initTaskService, tasksMap} from './taskService'
import {MESSAGE_TARGET_EXTENSION, MESSAGE_TO_EXTENSION_ADD_TASK, MESSAGE_TO_EXTENSION_GET_TASK} from '@/const'
/** const debug = (...args: any[]) => {
* 消息处理入口 console.debug('[Extension]', ...args)
* 注意需要异步sendResponse时返回true }
*/
chrome.runtime.onMessage.addListener((event, sender, sendResponse) => { const methods: {
console.debug('收到请求: ', event) [key: string]: (params: any, context: MethodContext) => Promise<any>
if (event.type === 'p') { // 发出http请求 } = {
const {url, options} = event [MESSAGE_TO_EXTENSION_ADD_TASK]: async (params, context) => {
// 发出请求
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 task: Task = { const task: Task = {
id: v4(), id: v4(),
startTime: Date.now(), startTime: Date.now(),
status: 'pending', status: 'pending',
def: event.taskDef, def: params.taskDef,
} }
tasksMap.set(task.id, task) tasksMap.set(task.id, task)
// 立即触发任务 // 立即触发任务
handleTask(task).catch(console.error) handleTask(task).catch(console.error)
return task
},
[MESSAGE_TO_EXTENSION_GET_TASK]: async (params, context) => {
// 返回任务信息 // 返回任务信息
sendResponse(task) const taskId = params.taskId
} else if (event.type === 'getTask') {
// 返回任务信息
const taskId = event.taskId
const task = tasksMap.get(taskId) const task = tasksMap.get(taskId)
if (task == null) { if (task == null) {
sendResponse({ return {
code: 'not_found', code: 'not_found',
}) }
return
} }
// 检测删除缓存 // 检测删除缓存
@@ -62,9 +40,68 @@ chrome.runtime.onMessage.addListener((event, sender, sendResponse) => {
} }
// 返回任务 // 返回任务
sendResponse({ return {
code: 'ok', code: 'ok',
task, 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,
}) })
} }
}) })

View File

@@ -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 APP_DOM_ID = 'bilibili-subtitle'
export const IFRAME_ID = 'bilibili-subtitle-iframe' 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 TASK_EXPIRE_TIME = 15*60*1000
export const PAGE_MAIN = 'main' export const PAGE_MAIN = 'main'

View File

@@ -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<PostMessageResponse> | undefined
export const injectWaiter = new Waiter<typeof postInjectMessage>(() => ({
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

View File

@@ -1,6 +1,8 @@
import {useAppDispatch, useAppSelector} from './redux' import {useAppDispatch, useAppSelector} from './redux'
import React, {useCallback} from 'react' import React, {useCallback} from 'react'
import {setNeedScroll, setReviewAction, setTempData} from '../redux/envReducer' import {setNeedScroll, setReviewAction, setTempData} from '../redux/envReducer'
import {sendInject} from '../util/biz_util'
import {MESSAGE_TO_INJECT_MOVE} from '../const'
const useSubtitle = () => { const useSubtitle = () => {
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
@@ -9,7 +11,7 @@ const useSubtitle = () => {
const reviewActions = useAppSelector(state => state.env.tempData.reviewActions) const reviewActions = useAppSelector(state => state.env.tempData.reviewActions)
const move = useCallback((time: number, togglePause: boolean) => { const move = useCallback((time: number, togglePause: boolean) => {
window.parent.postMessage({type: 'move', time, togglePause}, '*') sendInject(MESSAGE_TO_INJECT_MOVE, {time, togglePause})
//review action //review action
if (reviewed === undefined && !reviewAction) { if (reviewed === undefined && !reviewAction) {

View File

@@ -16,9 +16,11 @@ import {
setTempData, setTempData,
} from '../redux/envReducer' } from '../redux/envReducer'
import {EventBusContext} from '../Router' 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 {useInterval} from 'ahooks'
import {getModelMaxTokens, getWholeText} from '../util/biz_util' import {getModelMaxTokens, getWholeText} from '../util/biz_util'
import {sendInject} from '../util/biz_util'
import {MESSAGE_TO_INJECT_GET_SUBTITLE} from '../const'
/** /**
* Service是单例类似后端的服务概念 * Service是单例类似后端的服务概念
@@ -52,55 +54,6 @@ const useSubtitleService = () => {
} }
}, [reviewActions, dispatch, reviewed]) }, [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(() => { useEffect(() => {
if ((data != null) && data.body.length > 0) { if ((data != null) && data.body.length > 0) {
@@ -120,15 +73,30 @@ const useSubtitleService = () => {
// 获取 // 获取
useEffect(() => { useEffect(() => {
if (curInfo && !curFetched) { 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]) }, [curFetched, curInfo])
useEffect(() => { 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(() => { useInterval(() => {
window.parent.postMessage({type: 'getCurrentTime'}, '*') sendInject(MESSAGE_TO_INJECT_GET_VIDEO_STATUS, {}).then(status => {
dispatch(setCurrentTime(status.currentTime))
})
}, 500) }, 500)
// show translated text in the video // show translated text in the video
useEffect(() => { useEffect(() => {
if (hideOnDisableAutoTranslate && !autoTranslate) { if (hideOnDisableAutoTranslate && !autoTranslate) {
window.parent.postMessage({type: 'updateTransResult'}, '*') sendInject(MESSAGE_TO_INJECT_HIDE_TRANS, {})
return return
} }
const transResult = curIdx?transResults[curIdx]:undefined const transResult = curIdx?transResults[curIdx]:undefined
if (transResult?.code === '200' && transResult.data) { if (transResult?.code === '200' && transResult.data) {
window.parent.postMessage({type: 'updateTransResult', result: transResult.data}, '*') sendInject(MESSAGE_TO_INJECT_UPDATETRANSRESULT, {result: transResult.data})
} else { } else {
window.parent.postMessage({type: 'updateTransResult'}, '*') sendInject(MESSAGE_TO_INJECT_HIDE_TRANS, {})
} }
}, [autoTranslate, curIdx, hideOnDisableAutoTranslate, transResults]) }, [autoTranslate, curIdx, hideOnDisableAutoTranslate, transResults])
} }

View File

@@ -16,6 +16,8 @@ import {
import { import {
LANGUAGE_DEFAULT, LANGUAGE_DEFAULT,
LANGUAGES_MAP, LANGUAGES_MAP,
MESSAGE_TO_EXTENSION_ADD_TASK,
MESSAGE_TO_EXTENSION_GET_TASK,
PROMPT_DEFAULTS, PROMPT_DEFAULTS,
PROMPT_TYPE_ASK, PROMPT_TYPE_ASK,
PROMPT_TYPE_TRANSLATE, PROMPT_TYPE_TRANSLATE,
@@ -27,7 +29,7 @@ import {
} from '../const' } from '../const'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import {useMemoizedFn} from 'ahooks/es' 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' import {formatTime} from '../util/util'
const useTranslate = () => { const useTranslate = () => {
@@ -135,7 +137,7 @@ const useTranslate = () => {
} }
}) })
dispatch(addTransResults(result)) 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)) dispatch(addTaskId(task.id))
} }
} }
@@ -205,7 +207,7 @@ const useTranslate = () => {
console.debug('addSummarizeTask', taskDef) console.debug('addSummarizeTask', taskDef)
dispatch(setSummaryStatus({segmentStartIdx: segment.startIdx, type, status: 'pending'})) dispatch(setSummaryStatus({segmentStartIdx: segment.startIdx, type, status: 'pending'}))
dispatch(setLastSummarizeTime(Date.now())) 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(addTaskId(task.id))
} }
}, [dispatch, envData, summarizeLanguage.name, title]) }, [dispatch, envData, summarizeLanguage.name, title])
@@ -262,7 +264,7 @@ const useTranslate = () => {
id, id,
status: 'pending' 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(addTaskId(task.id))
} }
}, [dispatch, envData, summarizeLanguage.name, title]) }, [dispatch, envData, summarizeLanguage.name, title])
@@ -330,7 +332,7 @@ const useTranslate = () => {
}) })
const getTask = useCallback(async (taskId: string) => { 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') { if (taskResp.code === 'ok') {
console.debug('getTask', taskResp.task) console.debug('getTask', taskResp.task)
const task: Task = taskResp.task const task: Task = taskResp.task

View File

@@ -1,11 +1,81 @@
import {TOTAL_HEIGHT_DEF, HEADER_HEIGHT, TOTAL_HEIGHT_MIN, TOTAL_HEIGHT_MAX, IFRAME_ID} from '@/const' 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'
let totalHeight = TOTAL_HEIGHT_DEF 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 debug = (...args: any[]) => {
console.debug('[Inject]', ...args)
}
(function () {
const runtime: {
postMessageToApp?: (method: string, payload: PostMessagePayload) => Promise<PostMessageResponse>
// lastV?: string | null
// lastVideoInfo?: VideoInfo
fold: boolean
videoElement?: HTMLVideoElement
videoElementHeight: number
showTrans: boolean
curTrans?: string
} = {
fold: true,
videoElementHeight: TOTAL_HEIGHT_DEF,
showTrans: false,
}
const sendExtension = async <T = any>(method: string, params?: any) => {
return await chrome.runtime.sendMessage<MessageData, MessageResult>({
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 <T>(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 getVideoElement = () => {
const videoWrapper = document.getElementById('bilibili-player') const videoWrapper = document.getElementById('bilibili-player')
return videoWrapper?.querySelector('video') as HTMLVideoElement | undefined 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 () { const timerIframe = setInterval(function () {
var danmukuBox = document.getElementById('danmukuBox') var danmukuBox = document.getElementById('danmukuBox')
if (danmukuBox) { if (danmukuBox) {
@@ -35,7 +105,7 @@ const timerIframe = setInterval(function () {
//insert before first child //insert before first child
danmukuBox?.insertBefore(iframe, danmukuBox?.firstChild) danmukuBox?.insertBefore(iframe, danmukuBox?.firstChild)
console.debug('iframe inserted') debug('iframe inserted')
}, 1500) }, 1500)
} }
}, 1000) }, 1000)
@@ -100,17 +170,16 @@ const refreshVideoInfo = async () => {
pagesMap[page.page + ''] = page pagesMap[page.page + ''] = page
}) })
console.debug('refreshVideoInfo: ', aid, cid, pages, subtitles) debug('refreshVideoInfo: ', aid, cid, pages, subtitles)
//send setVideoInfo //send setVideoInfo
iframe.contentWindow?.postMessage({ sendApp(MESSAGE_TO_APP_SET_VIDEO_INFO, {
type: 'setVideoInfo',
url: location.origin + location.pathname, url: location.origin + location.pathname,
title, title,
aid, aid,
pages, pages,
infos: subtitles, infos: subtitles,
}, '*') })
} }
} }
} }
@@ -128,7 +197,7 @@ const refreshSubtitles = () => {
const cid = page.cid const cid = page.cid
if (aid !== lastAid || cid !== lastCid) { if (aid !== lastAid || cid !== lastCid) {
console.debug('refreshSubtitles', aid, cid) debug('refreshSubtitles', aid, cid)
lastAid = aid lastAid = aid
lastCid = cid lastCid = cid
@@ -139,85 +208,112 @@ const refreshSubtitles = () => {
.then(res => res.json()) .then(res => res.json())
.then(res => { .then(res => {
// console.log('refreshSubtitles: ', aid, cid, res) // console.log('refreshSubtitles: ', aid, cid, res)
iframe.contentWindow?.postMessage({ sendApp(MESSAGE_TO_APP_SET_INFOS, {
type: 'setInfos',
infos: res.data.subtitle.subtitles infos: res.data.subtitle.subtitles
}, '*') })
}) })
} }
} }
} }
// 监听消息 const updateIframeHeight = () => {
window.addEventListener("message", (event) => {
const {data} = event
if (data.type === 'fold') {
const iframe = document.getElementById(IFRAME_ID) as HTMLIFrameElement | undefined const iframe = document.getElementById(IFRAME_ID) as HTMLIFrameElement | undefined
if (iframe) { if (iframe != null) {
iframe.style.height = (data.fold ? HEADER_HEIGHT : totalHeight) + 'px' iframe.style.height = (runtime.fold ? HEADER_HEIGHT : runtime.videoElementHeight) + 'px'
} }
} }
if (data.type === 'move') { const methods: {
[key: string]: (params: any, context: MethodContext) => Promise<any>
} = {
[MESSAGE_TO_INJECT_FOLD]: async (params) => {
runtime.fold = params.fold
updateIframeHeight()
},
[MESSAGE_TO_INJECT_MOVE]: async (params) => {
const video = getVideoElement() const video = getVideoElement()
if (video) { if (video != null) {
video.currentTime = data.time video.currentTime = params.time
if (data.togglePause) { if (params.togglePause) {
video.paused ? video.play() : video.pause() video.paused ? video.play() : video.pause()
} }
} }
} },
[MESSAGE_TO_INJECT_GET_SUBTITLE]: async (params) => {
//刷新视频信息 let url = params.info.subtitle_url
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://')) { if (url.startsWith('http://')) {
url = url.replace('http://', 'https://') url = url.replace('http://', 'https://')
} }
fetch(url).then(res => res.json()).then(res => { return await fetch(url).then(res => res.json())
event.source?.postMessage({ },
data: { [MESSAGE_TO_INJECT_GET_VIDEO_STATUS]: async (params) => {
info: data.info,
data: res,
}, type: 'setSubtitle'
// @ts-ignore
}, '*')
})
}
if (data.type === 'getCurrentTime') {
const video = getVideoElement() const video = getVideoElement()
if (video) { if (video != null) {
event.source?.postMessage({ return {
data: { paused: video.paused,
currentTime: video.currentTime currentTime: video.currentTime
}, type: 'setCurrentTime'
// @ts-ignore
}, '*')
} }
} }
},
[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
if (data.type === 'getSettings') { let text = document.getElementById('trans-result-text')
const videoElement = getVideoElement() if (text) {
totalHeight = videoElement ? Math.min(Math.max(videoElement.offsetHeight, TOTAL_HEIGHT_MIN), TOTAL_HEIGHT_MAX) : TOTAL_HEIGHT_DEF text.innerHTML = runtime.curTrans??''
event.source?.postMessage({ } else {
data: { const container = document.getElementsByClassName('bpx-player-subtitle-panel-wrap')?.[0]
noVideo: !videoElement, if (container) {
totalHeight, const div = document.createElement('div')
}, type: 'setSettings' div.style.display = 'flex'
// @ts-ignore 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)
if (data.type === 'downloadAudio') { 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 html = document.getElementsByTagName('html')[0].innerHTML
const playInfo = JSON.parse(html.match(/window.__playinfo__=(.+?)<\/script/)?.[1] ?? '{}') const playInfo = JSON.parse(html.match(/window.__playinfo__=(.+?)<\/script/)?.[1] ?? '{}')
const audioUrl = playInfo.data.dash.audio[0].baseUrl const audioUrl = playInfo.data.dash.audio[0].baseUrl
@@ -228,37 +324,79 @@ window.addEventListener("message", (event) => {
a.download = `${title}.m4s` a.download = `${title}.m4s`
a.click() a.click()
}) })
},
} }
if (data.type === 'updateTransResult') { /**
const trans = data.result??'' * @param sendResponse No matter what is returned, this method will definitely be called.
let text = document.getElementById('trans-result-text') */
if (text) { const messageHandler = (event: MessageData, sender: chrome.runtime.MessageSender | null, sendResponse: (response?: MessageResult) => void) => {
text.innerHTML = trans 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 { } else {
const container = document.getElementsByClassName('bpx-player-subtitle-panel-wrap')?.[0] message = 'error: ' + JSON.stringify(err)
if (container) { }
const div = document.createElement('div') sendResponse({
div.style.display = 'flex' success: false,
div.style.justifyContent = 'center' code: 500,
div.style.margin = '2px' message,
text = document.createElement('text') })
text.id = 'trans-result-text' })
text.innerHTML = trans return true
text.style.fontSize = '1rem' } else {
text.style.padding = '5px' console.error('Unknown method:', event.method)
text.style.color = 'white' sendResponse({
text.style.background = 'rgba(0, 0, 0, 0.4)' success: false,
div.append(text) code: 501,
message: 'Unknown method: ' + event.method,
})
}
}
container.append(div) // listen message from app
} startListening({}).then(e => {
} const { postMessage, listenMessage, destroy } = e
text && (text.style.display = trans ? 'block' : 'none') runtime.postMessageToApp = postMessage
} listenMessage((method, params, sendResponse) => {
}, false); 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(() => { setInterval(() => {
refreshVideoInfo().catch(console.error) refreshVideoInfo().catch(console.error)
refreshSubtitles() refreshSubtitles()
}, 1000) }, 1000)
})()

20
src/typings.d.ts vendored
View File

@@ -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 { interface EnvData {
autoExpand?: boolean autoExpand?: boolean
flagDot?: boolean flagDot?: boolean

View File

@@ -2,6 +2,42 @@ import {APP_DOM_ID, CUSTOM_MODEL_TOKENS, MODEL_DEFAULT, MODEL_MAP, SUMMARIZE_TYP
import {isDarkMode} from '@kky002/kky-util' import {isDarkMode} from '@kky002/kky-util'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import {findIndex} from 'lodash-es' 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 <T = any>(method: string, params?: any) => {
return await chrome.runtime.sendMessage<MessageData, MessageResult>({
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 <T = any>(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')
}
}
/** /**
* 获取译文 * 获取译文