diff --git a/src/contents/index/App.tsx b/src/contents/index/App.tsx index deaa6ca6..08b1c6f3 100644 --- a/src/contents/index/App.tsx +++ b/src/contents/index/App.tsx @@ -69,15 +69,16 @@ function App(): JSX.Element { const { info, settings } = streamInfo - const { "settings.display": displaySettings } = settings + const { "settings.display": displaySettings, "settings.developer": developerSettings } = settings - // 狀態為離綫時,此處不需要顯示按鈕 + // 狀態為離綫時,除非强制啓動為 true + // 此處不需要顯示按鈕 // 離綫下載按鈕交給 feature UI 處理 if (info.status === 'offline') { return <> } - const screenStatus = useWebScreenChange(settings['settings.developer'].classes) + const screenStatus = useWebScreenChange(developerSettings.classes) const { bool: open, setFalse: closeDrawer, toggle } = useToggle(false) const tutorialRef = useRef() diff --git a/src/contents/index/mounter.tsx b/src/contents/index/mounter.tsx index d4b945de..5f6bbf59 100644 --- a/src/contents/index/mounter.tsx +++ b/src/contents/index/mounter.tsx @@ -8,7 +8,7 @@ import ContentContext from "~contexts/ContentContexts" import type { FeatureType } from "~features" import features from "~features" import type { Settings } from "~options/fragments" -import { shouldInit } from "~options/fragments" +import { shouldInit } from "~options/shouldInit" import { getStreamInfoByDom } from "~utils/bilibili" import { injectAdapter } from "~utils/inject" import { addBLiveMessageCommandListener, sendMessager } from "~utils/messaging" @@ -60,7 +60,10 @@ function createMountPoints(plasmo: PlasmoSpec, info: StreamInfo): RootMountable[ const portals = await hook(settings, info) // 返回禁用狀態的話則直接跳過渲染 - if (!portals) { + if (typeof portals === 'string') { + toast.warning(portals, { position: 'top-center' }) + return + } else if (!portals) { console.info(`房間 ${info.room} 已被 ${key} 功能禁用,已略過`) return } @@ -116,6 +119,7 @@ function createApp(roomId: string, plasmo: PlasmoSpec, info: StreamInfo): App { const settings = await getFullSettingStroage() const enabled = settings['settings.features'].enabledFeatures + const forceBoot = settings['settings.developer'].extra.forceBoot // 如果沒有取得直播資訊,就嘗試使用 DOM 取得 if (!info) { @@ -128,6 +132,11 @@ function createApp(roomId: string, plasmo: PlasmoSpec, info: StreamInfo): App { return } + // 強制啓動 + if (forceBoot) { + info.status = 'online' + } + if (!(await shouldInit(settings, info))) { console.info('不符合初始化條件,已略過') return @@ -141,7 +150,7 @@ function createApp(roomId: string, plasmo: PlasmoSpec, info: StreamInfo): App { toast.warning('检测到你尚未登录, 本扩展的功能将会严重受限, 建议你先登录B站。', { position: 'top-center' }) } - // hook adapter (only when online) + // hook adapter (only when online or forceBoot) if (info.status === 'online') { console.info('開始注入適配器....') const adapterType = settings["settings.capture"].captureMechanism diff --git a/src/features/index.ts b/src/features/index.ts index d45cb98a..cabe45f6 100644 --- a/src/features/index.ts +++ b/src/features/index.ts @@ -5,7 +5,7 @@ import * as recorder from './recorder' import type { StreamInfo } from '~api/bilibili' import type { Settings } from '~options/fragments' -export type FeatureHookRender = (settings: Readonly, info: StreamInfo) => Promise<(React.ReactPortal | React.ReactNode)[] | undefined> +export type FeatureHookRender = (settings: Readonly, info: StreamInfo) => Promise<(React.ReactPortal | React.ReactNode)[] | string | undefined> export type FeatureAppRender = React.FC<{}> diff --git a/src/features/jimaku/index.tsx b/src/features/jimaku/index.tsx index ecea7963..cbd8441c 100644 --- a/src/features/jimaku/index.tsx +++ b/src/features/jimaku/index.tsx @@ -95,7 +95,7 @@ const handler: FeatureHookRender = async (settings, info) => { if (noNativeVtuber && (await retryCatcher(() => isNativeVtuber(info.uid), 5))) { // do log console.info('檢測到為國V, 已略過') - return undefined // 返回 undefined 以禁用此功能 + return undefined // 返回 undefined 以禁用此功能且不發送任何警告 } return [ diff --git a/src/features/recorder/index.tsx b/src/features/recorder/index.tsx index 3425bf33..0efc8ffd 100644 --- a/src/features/recorder/index.tsx +++ b/src/features/recorder/index.tsx @@ -2,7 +2,6 @@ import RecorderFeatureContext from "~contexts/RecorderFeatureContext"; import type { FeatureHookRender } from "~features"; import { sendMessager } from "~utils/messaging"; import RecorderLayer from "./components/RecorderLayer"; -import { toast } from "sonner"; export const FeatureContext = RecorderFeatureContext @@ -10,8 +9,8 @@ const handler: FeatureHookRender = async (settings, info) => { const { error, data: urls } = await sendMessager('get-stream-urls', { roomId: info.room }) if (error) { - toast.error('启用快速切片功能失败: '+ error) - return undefined // disable the feature + console.warn('啟用快速切片功能失敗: ', error) + return '啟用快速切片功能失敗: '+ error // 返回 string 以顯示錯誤 } return [ diff --git a/src/options/components/SettingFragment.tsx b/src/options/components/SettingFragment.tsx index ee63e11b..9efb0af1 100644 --- a/src/options/components/SettingFragment.tsx +++ b/src/options/components/SettingFragment.tsx @@ -99,6 +99,7 @@ const SettingFragmentContent = forwardRef(function SettingFragmentContent ({ async saveSettings() { if (!isModified()) return // if not modified, do nothing + console.debug('saving: ', fragmentKey, stateProxy.state) await setSettingStorage>(fragmentKey, { ...stateProxy.state }) // set the settings to storage setBeforeSettings(deepCopy(stateProxy.state)) // update before settings so that to update check modified status }, diff --git a/src/options/fragments.ts b/src/options/fragments.ts index 8611c43b..8b9a25b2 100644 --- a/src/options/fragments.ts +++ b/src/options/fragments.ts @@ -6,10 +6,6 @@ import * as features from './fragments/features' import * as listings from './fragments/listings' import * as version from './fragments/version' -import type { StreamInfo } from '~api/bilibili' - - -type ShouldInit = (settings: Readonly, info: StreamInfo) => Promise interface SettingFragment { defaultSettings: Readonly @@ -26,18 +22,6 @@ export type Settings = { [K in keyof SettingFragments]: Schema } - -export async function shouldInit(settings: Settings, info: StreamInfo): Promise { - const shouldInits = Object.entries(fragments).map(([key, fragment]) => { - const shouldInit = (fragment as any).shouldInit as ShouldInit> - if (shouldInit) { - return shouldInit(settings[key], info) - } - return Promise.resolve(true) - }) - return (await Promise.all(shouldInits)).every(Boolean) -} - // also defined the order of the settings const fragments = { 'settings.features': features, @@ -48,6 +32,5 @@ const fragments = { 'settings.version': version } - export default fragments diff --git a/src/options/fragments/developer.tsx b/src/options/fragments/developer.tsx index 4b649ff1..3360bf7f 100644 --- a/src/options/fragments/developer.tsx +++ b/src/options/fragments/developer.tsx @@ -1,5 +1,5 @@ -import { Alert, Button, Input, Typography } from '@material-tailwind/react'; -import { Fragment, type ChangeEvent } from 'react'; +import { Alert, Button, Input, Switch, Typography } from '@material-tailwind/react'; +import { Fragment, type ChangeEvent, type ExoticComponent } from 'react'; import { toast } from 'sonner/dist'; import type { ExposeHandler, StateProxy } from "~hooks/binding"; import type { Leaves } from "~types/common"; @@ -33,6 +33,9 @@ export type SettingSchema = { chatUserId: string; chatDanmaku: string; }; + extra: { + forceBoot: boolean; + }; }; export const defaultSettings: Readonly = { @@ -61,13 +64,16 @@ export const defaultSettings: Readonly = { attr: { chatUserId: 'data-uid', // 聊天条 用户id 属性 chatDanmaku: 'data-danmaku' // 聊天条 弹幕 属性 + }, + extra: { + forceBoot: false // 在直播间下线时依然强制启动 } } export const title = '开发者相关' export const description = [ - '此设定区块是控制抓取元素的设定,这里的默认数值都是针对当前版本的B站页面。', + '此设定区块是控制抓取元素或其他实验性功能的设定,这里的默认数值都是针对当前版本的B站页面。', '若B站页面发生改版, 本扩展将无法抓取元素致无法运作。除了等待本扩展的修复版本更新外, 有JS开发经验的用户可以自行修改此区块的数值以适应新版面。', '至于没有JS开发经验的用户,则尽量不要碰这里的设定,否则有可能导致扩展无法正常运作。' ] @@ -82,6 +88,11 @@ type ElementDefinerList = { [title: string]: ElementDefiner[] } +type ComponentDefiner = { + Component: ExoticComponent, + handler: (e: ChangeEvent) => T, + props?: (prop: { key: string, label: string, value: T, handler: (e: ChangeEvent) => T }) => object +} const elementDefiners: ElementDefinerList = { "元素捕捉": [ @@ -167,13 +178,33 @@ const elementDefiners: ElementDefinerList = { label: "聊天条 弹幕 属性", key: "attr.chatDanmaku" }, + ], + "其他设定": [ + { + label: "在直播间下线时依然强制启动", + key: "extra.forceBoot" + } ] } +const componentDefiners: Record> = { + 'string': { + Component: Input, + handler: (e: ChangeEvent) => e.target.value + }, + 'boolean': { + Component: Switch, + handler: (e: ChangeEvent) => e.target.checked, + props: ({ key, label, value, handler }) => ({ + checked: value, + onChange: handler, + label: {label} + }) + } +} -function DeveloperSettings({ state, useHandler }: StateProxy): JSX.Element { - const handler = useHandler, string>((e) => e.target.value) +function DeveloperSettings({ state, useHandler }: StateProxy): JSX.Element { const alertIcon = ( ): JS const fetchDeveloper = async () => { if (!window.confirm('这将覆盖开发者相关所有目前设定。')) return - const fetching = (async function(){ + const fetching = (async function () { const { data, error } = await sendMessager('fetch-developer') if (error) throw new Error(error) await setSettingStorage('settings.developer', data) @@ -200,7 +231,7 @@ function DeveloperSettings({ state, useHandler }: StateProxy): JS toast.promise(fetching, { loading: '正在获取远端最新开发者设定...', success: '已成功获取最新版本,请重新加载网页。', - error: (err) => '获取最新版本失败: '+err.message + error: (err) => '获取最新版本失败: ' + err.message }) await fetching } @@ -226,16 +257,23 @@ function DeveloperSettings({ state, useHandler }: StateProxy): JS {title} - {definers.map(({ label, key }) => ( -