From 892c3e88f7ffa9ac79e03bd1dab6721f89fdec57 Mon Sep 17 00:00:00 2001 From: apades <36807043+apades@users.noreply.github.com> Date: Mon, 23 Mar 2026 18:21:05 +0800 Subject: [PATCH] feat: add support for launching PIP with a link and update context menu handling --- src/background/index.ts | 67 +++++++-- src/components/VideoPlayerV2/hooks.ts | 2 +- src/components/VideoPlayerV2/index.tsx | 18 ++- src/contents/main.ts | 62 ++++++++- src/core/VideoPlayer/CanvasVideoPlayer.tsx | 3 + src/core/VideoPlayer/HtmlVideoPlayer.tsx | 6 +- src/core/VideoPlayer/VideoPlayerBase.ts | 2 +- src/core/WebProvider/DocPIPWebProvider.ts | 98 ++++++++------ ...hPIPWithReplaceModeFromLinkWebProvider.tsx | 128 ++++++++++++++++++ src/core/WebProvider/WebProvider.ts | 34 +++-- src/core/WebProvider/index.ts | 2 + src/core/event.ts | 4 + src/locales/zh_CN.json | 4 +- src/shared/contextmenu.ts | 5 + src/shared/postMessageEvent.ts | 11 ++ src/shared/webextEvent.ts | 3 + src/shim.d.ts | 6 + src/store/config/index.tsx | 4 + src/store/playerConfig.ts | 1 + src/types/config.ts | 1 + src/utils/windowMessages.ts | 2 + 21 files changed, 382 insertions(+), 81 deletions(-) create mode 100644 src/core/WebProvider/LaunchPIPWithReplaceModeFromLinkWebProvider.tsx create mode 100644 src/shared/contextmenu.ts diff --git a/src/background/index.ts b/src/background/index.ts index 03b07c4d..d89a90b6 100644 --- a/src/background/index.ts +++ b/src/background/index.ts @@ -1,10 +1,12 @@ import getDanmakuGetter from '@pkgs/danmakuGetter/getDanmakuGetter' +import isDev from '@root/shared/isDev' import { FLOAT_BTN_HIDDEN, LOCALE, NEED_RELOAD as NEED_RELOAD_PAGE, } from '@root/shared/storeKey' import WebextEvent from '@root/shared/webextEvent' +import { tryCatch } from '@root/utils' import { t } from '@root/utils/i18n' import { getBrowserLocalStorage, @@ -13,15 +15,18 @@ import { useBrowserLocalStorage, useBrowserSyncStorage, } from '@root/utils/storage' +import { + FLOAT_BTN_ID, + SETTING_ID, + LINK_CONTEXTMENU_OPEN_PIP_ID, +} from '@root/shared/contextmenu' +import { autorun } from 'mobx' import { v4 as uuid } from 'uuid' import { onMessage, sendMessage } from 'webext-bridge/background' import Browser from 'webextension-polyfill' +import { WS_PORT } from '../../scripts/shared' import './commands' import './docPIP' -import isDev from '@root/shared/isDev' -import { tryCatch } from '@root/utils' -import { WS_PORT } from '../../scripts/shared' -// import '../entry/vite.bg' console.log('run bg') getBrowserLocalStorage(LOCALE).then((locale) => { @@ -58,7 +63,7 @@ if (isDev) { if (isDev) { // 好像只要有这个就可以keep alive了 - chrome.runtime.onConnect.addListener((port) => { + Browser.runtime.onConnect.addListener((port) => { port.onMessage.addListener((msg) => { // keep alive }) @@ -73,13 +78,13 @@ getBrowserLocalStorage(NEED_RELOAD_PAGE).then(async (needReload) => { }) async function reloadPage() { - const tabs = await chrome.tabs.query({ active: true }) + const tabs = await Browser.tabs.query({ active: true }) console.log('tabs', tabs) tabs.forEach((tab) => { if (!tab.id) return if (!tab.url) return - chrome.scripting.executeScript({ + Browser.scripting.executeScript({ target: { tabId: tab.id }, func: () => { location.reload() @@ -122,14 +127,19 @@ onMessage(WebextEvent.bgFetch, async (req) => { onMessage(WebextEvent.getup, () => 'hello') +onMessage(WebextEvent.updateContextMenu, ({ data }) => { + Browser.contextMenus.update(data.id, data.data) +}) + const getTabCapturePermission = () => - new Promise((res) => { - chrome.permissions.contains({ permissions: ['tabCapture'] }, (rs) => { - if (rs) return res(true) - chrome.permissions.request({ permissions: ['tabCapture'] }, (rs) => { - res(rs) - }) + new Promise(async (res) => { + const rs = await Browser.permissions.contains({ + permissions: ['tabCapture'], }) + if (rs) return res(true) + res( + await Browser.permissions.request({ permissions: ['tabCapture'] as any }), + ) }) onMessage(WebextEvent.getTabCapturePermission, getTabCapturePermission) @@ -208,8 +218,6 @@ onMessage(WebextEvent.stopGetDanmaku, ({ data }) => { } }) -const FLOAT_BTN_ID = 'FLOAT_BTN_ID', - SETTING_ID = 'SETTING_ID' Browser.runtime.onInstalled.addListener(() => { Browser.contextMenus.create({ contexts: ['action'], @@ -222,8 +230,20 @@ Browser.runtime.onInstalled.addListener(() => { title: t('menu.openSetting'), id: SETTING_ID, }) + Browser.contextMenus.create({ + contexts: ['link'], + id: LINK_CONTEXTMENU_OPEN_PIP_ID, + title: /* t('menu.openPIP') */ '打开画中画', + }) }) +// autorun(() => { +// console.log( +// 'config.disableOpenPIPInLinkMenu', +// config.disableOpenPIPInLinkMenu, +// ) +// }) + // 很奇怪的需要延迟点才不会触发'Cannot find menu item with id' setTimeout(() => { useBrowserLocalStorage(LOCALE, (locale) => { @@ -244,6 +264,7 @@ setTimeout(() => { }, 50) Browser.contextMenus.onClicked.addListener((info, tab) => { + console.log('click info', info) switch (info.menuItemId) { case FLOAT_BTN_ID: { setBrowserSyncStorage(FLOAT_BTN_HIDDEN, !info.checked) @@ -256,6 +277,22 @@ Browser.contextMenus.onClicked.addListener((info, tab) => { context: 'content-script', }) } + break + } + case LINK_CONTEXTMENU_OPEN_PIP_ID: { + if (tab?.id) { + sendMessage( + WebextEvent.launchPIPWithReplaceModeFromLink, + { + openUrl: info.linkUrl ?? '', + }, + { + tabId: tab?.id, + context: 'content-script', + }, + ) + } + break } } }) diff --git a/src/components/VideoPlayerV2/hooks.ts b/src/components/VideoPlayerV2/hooks.ts index dbebeaa6..bdd8035e 100644 --- a/src/components/VideoPlayerV2/hooks.ts +++ b/src/components/VideoPlayerV2/hooks.ts @@ -9,7 +9,7 @@ import { useMemoizedFn } from 'ahooks' import vpContext from './context' export const useTogglePlayState = () => { - const { webVideo, isLive } = useContext(vpContext) + const { webVideo, isLive, eventBus } = useContext(vpContext) const togglePlayState = useMemoizedFn(async (type?: 'play' | 'pause') => { if (!webVideo) return diff --git a/src/components/VideoPlayerV2/index.tsx b/src/components/VideoPlayerV2/index.tsx index ce9c7de8..f9ce1ed9 100644 --- a/src/components/VideoPlayerV2/index.tsx +++ b/src/components/VideoPlayerV2/index.tsx @@ -15,7 +15,13 @@ import useOpenIsolationModal from '@root/hook/useOpenIsolationModal' import useTargetEventListener from '@root/hook/useTargetEventListener' import PostMessageEvent from '@root/shared/postMessageEvent' import configStore, { ReplacerDbClickAction } from '@root/store/config' -import { isDocPIP, isIframe, ownerWindow, wait } from '@root/utils' +import { + createElement, + isDocPIP, + isIframe, + ownerWindow, + wait, +} from '@root/utils' import { hasParent } from '@root/utils/dom' import screenfull from '@root/utils/screenfull' import { Omit } from '@root/utils/typeUtils' @@ -193,6 +199,12 @@ const VideoPlayerV2Inner = observer( return toFullInWeb }) }) + useOnce(() => + eventBus.on2(PlayerEvent.toggleFullInWeb, () => { + console.log('toggleFullInWeb') + toggleFullInWeb() + }), + ) const toggleFullscreen = useMemoizedFn(() => { if (screenfull.isFullscreen) { screenfull.exit() @@ -355,6 +367,10 @@ const VideoPlayerV2Inner = observer( } }) + useOnce(() => { + eventBus.emit(PlayerEvent.videoPlayerInitd) + }) + const el = (
{ + // requestVideoPIP() + playerConfig.forceDocPIPRenderType = + DocPIPRenderType.launchPIPWithReplaceModeFromLink + playerConfig.replaceModeFromLinkUrl = data.openUrl + + const provider = new CommonProvider() + window.provider = provider + provider.openPlayer() + }) } else { // 处理top发来的请求检测video标签 onPostMessage(PostMessageEvent.detectVideo_req, () => { @@ -49,12 +64,51 @@ if (isTop) { }), ) }) + + // 2. 从LaunchPIPWithReplaceModeFromLinkWebProvider中传事件到位于docPIP iframe的该cs中,启动replaceWebVideoDom模式 + onPostMessage(PostMessageEvent.openReplaceModePlayer, async (_, source) => { + const videoEl = document.querySelector('video') + console.log('openReplaceModePlayer', videoEl) + if (!videoEl) + return postMessageToChild( + PostMessageEvent.openReplaceModePlayer_resp, + { + isOk: false, + reason: '找不到video元素', + }, + source, + ) + + playerConfig.forceDocPIPRenderType = DocPIPRenderType.replaceWebVideoDom + const provider = getWebProvider() + const [container, vel, isFixed] = getVideoElInitFloatButtonData(videoEl) + playerConfig.topContainerEl = container + playerConfig.isFixedPos = isFixed + window.provider = provider + setTimeout(async () => { + await provider.openPlayer({ + videoEl, + }) + const unListen = eventBus.on2(PlayerEvent.videoPlayerInitd, () => { + unListen() + eventBus.emit(PlayerEvent.toggleFullInWeb) + postMessageToChild( + PostMessageEvent.openReplaceModePlayer_resp, + { + isOk: true, + }, + source, + ) + }) + }, 50) + }) + // onPostMessage(PostMessageEvent.openReplaceModePlayer, () => {}) } function main() { let provider: WebProvider | undefined let getProvider = () => { - provider = _getWebProvider() + provider = getWebProvider() window.provider = provider return provider } @@ -422,7 +476,7 @@ function main() { console.log('🟡 No support mediaSession action enterpictureinpicture') } - window.getWebProvider = _getWebProvider + window.getWebProvider = getWebProvider } if (isDev) { diff --git a/src/core/VideoPlayer/CanvasVideoPlayer.tsx b/src/core/VideoPlayer/CanvasVideoPlayer.tsx index 0f290f68..c245ac66 100644 --- a/src/core/VideoPlayer/CanvasVideoPlayer.tsx +++ b/src/core/VideoPlayer/CanvasVideoPlayer.tsx @@ -2,6 +2,7 @@ import { createElement } from '@root/utils' import { ERROR_MSG } from '@root/shared/errorMsg' import { CanvasDanmakuEngine } from '../danmaku/DanmakuEngine' import CanvasDanmakuVideo from '../danmaku/DanmakuEngine/canvasDanmaku/CanvasDanmakuVideo' +import { PlayerEvent } from '../event' import VideoPlayerBase from './VideoPlayerBase' export class CanvasVideoPlayer extends VideoPlayerBase { @@ -57,5 +58,7 @@ export class CanvasVideoPlayer extends VideoPlayerBase { this.bindEvent() } } + + this.emit(PlayerEvent.videoPlayerInitd) } } diff --git a/src/core/VideoPlayer/HtmlVideoPlayer.tsx b/src/core/VideoPlayer/HtmlVideoPlayer.tsx index fd1e7a71..d0682645 100644 --- a/src/core/VideoPlayer/HtmlVideoPlayer.tsx +++ b/src/core/VideoPlayer/HtmlVideoPlayer.tsx @@ -14,10 +14,6 @@ import CanvasVideo from '../CanvasVideo' import { PlayerEvent } from '../event' import VideoPlayerBase, { supportOnVideoChangeTypes } from './VideoPlayerBase' -const docPIPStyleEl = createElement('style', { - innerText: 'html, body { height: 100% }', -}) - export class HtmlVideoPlayer extends VideoPlayerBase { playerRootEl?: HTMLElement @@ -66,7 +62,7 @@ export class HtmlVideoPlayer extends VideoPlayerBase { }) this.playerRootEl = createElement('div', { className: 'h-full', - children: [root, docPIPStyleEl], + children: [root], }) const reactRoot = createRoot(root) diff --git a/src/core/VideoPlayer/VideoPlayerBase.ts b/src/core/VideoPlayer/VideoPlayerBase.ts index e34a916d..7b8a5a25 100644 --- a/src/core/VideoPlayer/VideoPlayerBase.ts +++ b/src/core/VideoPlayer/VideoPlayerBase.ts @@ -122,7 +122,7 @@ export default class VideoPlayerBase } }) - this.emit(PlayerEvent.videoPlayerInitd) + // this.emit(PlayerEvent.videoPlayerInitd) } async unload() { this.emit(PlayerEvent.videoPlayerBeforeUnload) diff --git a/src/core/WebProvider/DocPIPWebProvider.ts b/src/core/WebProvider/DocPIPWebProvider.ts index 1a24f038..ef2959f4 100644 --- a/src/core/WebProvider/DocPIPWebProvider.ts +++ b/src/core/WebProvider/DocPIPWebProvider.ts @@ -20,7 +20,7 @@ export default class DocPIPWebProvider extends WebProvider { pipWindow?: Window - override async onOpenPlayer() { + protected async initPipWindow() { // 在标题后添加 ' - PIP' const title = document.title const pipTitle = title + ' - PIP' @@ -28,42 +28,61 @@ export default class DocPIPWebProvider extends WebProvider { // 获取应该有的docPIP宽高 const pipWindowConfig = await getBrowserSyncStorage(PIP_WINDOW_CONFIG) - let width = pipWindowConfig?.width ?? this.webVideo.clientWidth, - height = pipWindowConfig?.height ?? this.webVideo.clientHeight + let width = pipWindowConfig?.width ?? this.webVideo?.clientWidth, + height = pipWindowConfig?.height ?? this.webVideo?.clientHeight console.log('[docPIP_WH] pipWindowConfig', pipWindowConfig) - // cw / ch = vw / vh - const vw = this.webVideo.videoWidth, - vh = this.webVideo.videoHeight - switch (configStore.videoNoBorder) { - // cw = vw / vh * ch - case videoBorderType.height: { - width = (vw / vh) * height - break - } - // ch = vh / vw * cw - case videoBorderType.width: { - height = (vh / vw) * width - break + if (this.webVideo) { + // cw / ch = vw / vh + const vw = this.webVideo.videoWidth, + vh = this.webVideo.videoHeight + + switch (configStore.videoNoBorder) { + // cw = vw / vh * ch + case videoBorderType.height: { + width = (vw / vh) * height + break + } + // ch = vh / vw * cw + case videoBorderType.width: { + height = (vh / vw) * width + break + } } } await sendMessage(WebextEvent.beforeStartPIP, null) - await this.miniPlayer.init() - const playerEl = this.miniPlayer.playerRootEl - if (!playerEl) { - console.error('不正常的miniPlayer.init(),没有 playerEl', this.miniPlayer) - throw Error('不正常的miniPlayer.init()') - } - console.log('[docPIP_WH] real width height', { width, height }) const pipWindow = await window.documentPictureInPicture.requestWindow({ width, height, }) this.pipWindow = pipWindow + // docPIP有自带的样式,需要覆盖掉 + const docPIPRootStyle = createElement('style', { + innerHTML: ` +html, body { height: 100% } +body{ + margin: 0; + background-color: #000; +} +video{ + width: 100%; + height: 100%; +} +canvas{ + position: fixed; + top: 0; + left: 0; + z-index: 10; + width: 100%; + pointer-events: none; +}`, + }) + pipWindow.document.body.appendChild(docPIPRootStyle) + // 这里await会莫名其妙使webVideo被暂停 sendMessage(WebextEvent.afterStartPIP, { width: pipWindow.innerWidth, @@ -234,28 +253,19 @@ export default class DocPIPWebProvider extends WebProvider { } catch (error) {} }) - pipWindow.document.body.appendChild(playerEl) + console.log('[docPIP_WH] real width height', { width, height }) + } - // docPIP有自带的样式,需要覆盖掉 - const docPIPRootStyle = createElement('style', { - innerHTML: `body{ - margin: 0; - background-color: #000; -} -video{ - width: 100%; - height: 100%; -} -canvas{ - position: fixed; - top: 0; - left: 0; - z-index: 10; - width: 100%; - pointer-events: none; -}`, - }) - playerEl.appendChild(docPIPRootStyle) + override async onOpenPlayer() { + await this.initPipWindow() + await this.miniPlayer.init() + const playerEl = this.miniPlayer.playerRootEl + if (!playerEl) { + console.error('不正常的miniPlayer.init(),没有 playerEl', this.miniPlayer) + throw Error('不正常的miniPlayer.init()') + } + + this.pipWindow?.document.body.appendChild(playerEl) } override close(): void { diff --git a/src/core/WebProvider/LaunchPIPWithReplaceModeFromLinkWebProvider.tsx b/src/core/WebProvider/LaunchPIPWithReplaceModeFromLinkWebProvider.tsx new file mode 100644 index 00000000..627c2195 --- /dev/null +++ b/src/core/WebProvider/LaunchPIPWithReplaceModeFromLinkWebProvider.tsx @@ -0,0 +1,128 @@ +import { LoadingOutlined } from '@ant-design/icons' +import AppRoot from '@root/components/AppRoot' +import PostMessageEvent from '@root/shared/postMessageEvent' +import playerConfig from '@root/store/playerConfig' +import { createElement } from '@root/utils' +import Events2 from '@root/utils/Events2' +import { onPostMessage, postMessageToChild } from '@root/utils/windowMessages' +import classNames from 'classnames' +import { FC, useEffect, useRef, useState } from 'react' +import { createRoot } from 'react-dom/client' +import { sendMessage } from 'webext-bridge/content-script' +import { HtmlVideoPlayer } from '../VideoPlayer/HtmlVideoPlayer' +import DocPIPWebProvider from './DocPIPWebProvider' + +export default class LaunchPIPWithReplaceModeFromLinkWebProvider extends DocPIPWebProvider { + declare miniPlayer: HtmlVideoPlayer + protected override MiniPlayer = HtmlVideoPlayer + iframeEventBus = new Events2<{ fail: string }>() + + override get webVideo() { + return null as any + } + + private initLoadingView() { + if (!this.pipWindow) throw Error('没初始化pipWindow') + + const LoadingView: FC = () => { + const [isOk, setOk] = useState(false) + const iframeRef = useRef(null) + const [failReason, setFailReason] = useState('') + + useEffect(() => { + console.log('iframeRef.current', iframeRef.current) + if (!iframeRef.current) return + iframeRef.current.addEventListener('error', () => { + this.iframeEventBus.emit('fail', 'iframe加载失败') + setFailReason('iframe加载失败') + }) + + iframeRef.current.addEventListener('load', () => { + setTimeout(() => { + postMessageToChild( + PostMessageEvent.openReplaceModePlayer, + undefined, + iframeRef.current!.contentWindow!, + ) + }, 500) + + // new Promise(async()=>{ + + // }) + const unListen = onPostMessage( + PostMessageEvent.openReplaceModePlayer_resp, + (data) => { + unListen() + if (!data.isOk) { + this.iframeEventBus.emit('fail', data.reason) + setFailReason(data.reason) + } else { + setOk(true) + } + }, + ) + }) + }, [iframeRef.current]) + + return ( + + +
+
+ {failReason && ( +
{failReason}
+ )} + {!isOk && !failReason && ( +
+
+ +
+
+ )} +
+
+ ) + } + const root = createElement('div', { style: { height: '100%' } }) + const reactRoot = createRoot(root) + reactRoot.render() + this.pipWindow.document.body.appendChild(root) + } + override async onInit() { + await this.initPipWindow() + await this.initLoadingView() + } + + // 只需要docPIP的外壳操作代码,播放器的去掉 + override async openPlayer(props?: { videoEl?: HTMLVideoElement }) { + await this.init() + sendMessage('PIP-active', { name: 'PIP-active' }) + } + override async onOpenPlayer() { + // clear DocPIPWebProvider onOpenPlayer method + } + + override onUnload() { + this.iframeEventBus.offAll() + super.onUnload() + } +} diff --git a/src/core/WebProvider/WebProvider.ts b/src/core/WebProvider/WebProvider.ts index ce83ce9f..cb657987 100644 --- a/src/core/WebProvider/WebProvider.ts +++ b/src/core/WebProvider/WebProvider.ts @@ -13,6 +13,7 @@ import { checkIsLive } from '@root/utils/video' import { SettingDanmakuEngine } from '@root/store/config/danmaku' import WebextEvent from '@root/shared/webextEvent' import { DocPIPRenderType, Position } from '@root/types/config' +import { OrPromise } from '@root/utils/typeUtils' import { CanvasDanmakuEngine, DanmakuEngine, @@ -27,7 +28,12 @@ import { EventBus, PlayerEvent } from '../event' import { SideSwitcher } from '../SideSwitcher' import IronKinokoEngine from '../danmaku/DanmakuEngine/IronKinoko/IronKinokoEngine' import VideoPreviewManager from '../VideoPreviewManager' -import { CanvasPIPWebProvider, DocPIPWebProvider, ReplacerWebProvider } from '.' +import { + CanvasPIPWebProvider, + DocPIPWebProvider, + ReplacerWebProvider, + LaunchPIPWithReplaceModeFromLinkWebProvider, +} from '.' // ? 不知道为什么不能集中一起放这里,而且放这里是3个empty😅 // const FEAT_PROVIDER_LIST = [ @@ -67,13 +73,21 @@ export default abstract class WebProvider constructor() { super() if ( - [DocPIPWebProvider, CanvasPIPWebProvider, ReplacerWebProvider].includes( - Object.getPrototypeOf(this).constructor, - ) + [ + DocPIPWebProvider, + CanvasPIPWebProvider, + ReplacerWebProvider, + LaunchPIPWithReplaceModeFromLinkWebProvider, + ].includes(Object.getPrototypeOf(this).constructor) ) return this const provider = (() => { + if ( + playerConfig.forceDocPIPRenderType === + DocPIPRenderType.launchPIPWithReplaceModeFromLink + ) + return new LaunchPIPWithReplaceModeFromLinkWebProvider() if ( (playerConfig.forceDocPIPRenderType || configStore.docPIP_renderType) === DocPIPRenderType.replaceWebVideoDom @@ -84,15 +98,17 @@ export default abstract class WebProvider })() const rootPrototype = + getDeepPrototype(this, LaunchPIPWithReplaceModeFromLinkWebProvider) || getDeepPrototype(this, DocPIPWebProvider) || getDeepPrototype(this, CanvasPIPWebProvider) || getDeepPrototype(this, ReplacerWebProvider) || getDeepPrototype(this, WebProvider) + Object.setPrototypeOf(rootPrototype, provider) return this } - init() { + async init() { this.danmakuEngine = (() => { if (configStore.useHtmlDanmaku && configStore.useDocPIP) { if (configStore.htmlDanmakuEngine === SettingDanmakuEngine.IronKinoko) @@ -104,10 +120,10 @@ export default abstract class WebProvider this.subtitleManager = new SubtitleManager() - this.onInit() + await this.onInit() this.active = true } - onInit(): void {} + onInit(): OrPromise {} /**播放器初始化完毕后触发 */ onPlayerInitd(): void {} @@ -131,8 +147,8 @@ export default abstract class WebProvider /**打开播放器 */ async openPlayer(props?: { videoEl?: HTMLVideoElement }) { - if (!navigator.userActivation.isActive) return - this.init() + // if (!navigator.userActivation.isActive) return + await this.init() this.webVideo = props?.videoEl ?? this.getVideoEl() this.injectVideoEventsListener(this.webVideo) this.bindCommandsEvent() diff --git a/src/core/WebProvider/index.ts b/src/core/WebProvider/index.ts index 8bc1e75c..2e269873 100644 --- a/src/core/WebProvider/index.ts +++ b/src/core/WebProvider/index.ts @@ -6,6 +6,7 @@ import DocPIPWebProvider from './DocPIPWebProvider' import CanvasPIPWebProvider from './CanvasPIPWebProvider' import HtmlDanmakuProvider from './htmlDanmakuProvider' import ReplacerWebProvider from './ReplacerWebProvider' +import LaunchPIPWithReplaceModeFromLinkWebProvider from './LaunchPIPWithReplaceModeFromLinkWebProvider' export { WebProvider, @@ -13,4 +14,5 @@ export { CanvasPIPWebProvider, HtmlDanmakuProvider, ReplacerWebProvider, + LaunchPIPWithReplaceModeFromLinkWebProvider, } diff --git a/src/core/event.ts b/src/core/event.ts index 37957a87..f6d54928 100644 --- a/src/core/event.ts +++ b/src/core/event.ts @@ -56,6 +56,10 @@ export const PlayerEvent = { videoSrcChanged: 'videoSrcChanged', volumeChanged: 'volumeChanged', + + toggleFullInWeb: 'toggleFullInWeb', + videoPlay: 'videoPlay', + videoPause: 'videoPause', } as const type ToastArgs = Parameters diff --git a/src/locales/zh_CN.json b/src/locales/zh_CN.json index 4e76ce5f..6aba827f 100644 --- a/src/locales/zh_CN.json +++ b/src/locales/zh_CN.json @@ -120,6 +120,7 @@ "showReplacerBtn": "显示替换视频按钮", "showReplacerBtnDesc": "将网页中的视频播放器替换成插件的视频播放器", "replacerDbClickAction": "双击动作", + "disableOpenPIPInLinkMenu": "隐藏右键菜单打开画中画", "features": "功能", "bpPlaybackRate": "播放速度", "bpResize": "自动调整大小", @@ -185,7 +186,8 @@ }, "menu": { "showFloatBtn": "显示浮动按钮", - "openSetting": "打开设置" + "openSetting": "打开设置", + "openPIP": "打开画中画" }, "floatButton": { "closeTips": "您可以在扩展菜单中重新打开浮动按钮", diff --git a/src/shared/contextmenu.ts b/src/shared/contextmenu.ts new file mode 100644 index 00000000..0ec7976d --- /dev/null +++ b/src/shared/contextmenu.ts @@ -0,0 +1,5 @@ +const FLOAT_BTN_ID = 'FLOAT_BTN_ID', + SETTING_ID = 'SETTING_ID', + LINK_CONTEXTMENU_OPEN_PIP_ID = 'LINK_MENU' + +export { FLOAT_BTN_ID, SETTING_ID, LINK_CONTEXTMENU_OPEN_PIP_ID } diff --git a/src/shared/postMessageEvent.ts b/src/shared/postMessageEvent.ts index a513bc49..8b6e50aa 100644 --- a/src/shared/postMessageEvent.ts +++ b/src/shared/postMessageEvent.ts @@ -18,6 +18,8 @@ enum PostMessageEvent { fullInWeb_eventProxy = 'fullInWeb_eventProxy', closeDocPIP = 'closeDocPIP', asyncData = 'asyncData', + openReplaceModePlayer = 'openReplaceModePlayer', + openReplaceModePlayer_resp = 'openReplaceModePlayer_resp', } export type BaseVideoState = { @@ -37,6 +39,15 @@ export type VideoPosData = { } export interface PostMessageProtocolMap { + [PostMessageEvent.openReplaceModePlayer]: void + [PostMessageEvent.openReplaceModePlayer_resp]: + | { + isOk: true + } + | { + isOk: false + reason: string + } [PostMessageEvent.startPIPFromFloatButton]: { cropTarget?: CropTarget restrictionTarget?: RestrictionTarget diff --git a/src/shared/webextEvent.ts b/src/shared/webextEvent.ts index a30eb556..905c829a 100644 --- a/src/shared/webextEvent.ts +++ b/src/shared/webextEvent.ts @@ -22,6 +22,9 @@ enum WebextEvent { closePIP = 'closePIP', reloadPage = 'reloadPage', + + launchPIPWithReplaceModeFromLink = 'launchPIPWithReplaceModeFromLink', + updateContextMenu = 'updateContextMenu', } export default WebextEvent diff --git a/src/shim.d.ts b/src/shim.d.ts index 54e619d6..bb0d5e5b 100644 --- a/src/shim.d.ts +++ b/src/shim.d.ts @@ -1,5 +1,6 @@ import { ProtocolWithReturn } from 'webext-bridge' import { Props as DanmakuGetterProps } from '@pkgs/danmakuGetter/DanmakuGetter' +import { Menus } from 'webextension-polyfill' import WebextEvent from './shared/webextEvent' import { DanmakuInitData } from './core/danmaku/DanmakuEngine' @@ -39,6 +40,7 @@ declare module 'webext-bridge' { | { state: string; errType?: string } > [WebextEvent.openSetting]: void + [WebextEvent.launchPIPWithReplaceModeFromLink]: { openUrl: string } [WebextEvent.moveDocPIPPos]: { x: number; y: number; docPIPWidth: number } [WebextEvent.resizeDocPIP]: { width: number @@ -52,5 +54,9 @@ declare module 'webext-bridge' { [WebextEvent.afterStartPIP]: { width: number } [WebextEvent.reloadPage]: null + [WebextEvent.updateContextMenu]: { + id: string + data: Menus.UpdateUpdatePropertiesType + } } } diff --git a/src/store/config/index.tsx b/src/store/config/index.tsx index 853f9490..e7e5f512 100644 --- a/src/store/config/index.tsx +++ b/src/store/config/index.tsx @@ -241,6 +241,10 @@ export const baseConfigMap = { relateBy: 'showReplacerBtn', relateByValue: true, }), + disableOpenPIPInLinkMenu: config({ + defaultValue: false, + label: t('settingPanel.disableOpenPIPInLinkMenu'), + }), exportImportSettings: config({ defaultValue: '', label: t('settingPanel.exportImportSettings' as any), diff --git a/src/store/playerConfig.ts b/src/store/playerConfig.ts index f456da1f..14a3e70a 100644 --- a/src/store/playerConfig.ts +++ b/src/store/playerConfig.ts @@ -12,6 +12,7 @@ type PlayerConfig = { webRTCMediaStream: MediaStream topContainerEl: HTMLElement isFixedPos: boolean + replaceModeFromLinkUrl: string }> const playerConfig: PlayerConfig = { diff --git a/src/types/config.ts b/src/types/config.ts index 8644ee1b..cd410b10 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -63,6 +63,7 @@ export enum DocPIPRenderType { injectMediaSource = 'injectMediaSource', replaceWebVideoDom = 'replaceWebVideoDom', + launchPIPWithReplaceModeFromLink = 'launchPIPWithReplaceModeFromLink', } export enum Position { diff --git a/src/utils/windowMessages.ts b/src/utils/windowMessages.ts index d0c4fe30..650a9322 100644 --- a/src/utils/windowMessages.ts +++ b/src/utils/windowMessages.ts @@ -56,11 +56,13 @@ export function postMessageToChild< const eventSource = new Events2() window.addEventListener('message', (event) => { if (event.data?.ID !== ID) return + console.log('message from', event.data, location.href) eventSource.emit(event.data.type, { data: event.data.data, source: event.source, }) }) +window._eventSource = eventSource export function onPostMessage( type: T,