From 3018644aa23939a1d675cdb3373c800f07db5a18 Mon Sep 17 00:00:00 2001 From: Mr-Quin <8700123+Mr-Quin@users.noreply.github.com> Date: Tue, 17 Mar 2026 00:01:23 -0700 Subject: [PATCH 1/4] sa --- .../ui/floatingPanel/pages/DebugPage.tsx | 407 ++++++++++++++++-- 1 file changed, 382 insertions(+), 25 deletions(-) diff --git a/packages/danmaku-anywhere/src/content/controller/ui/floatingPanel/pages/DebugPage.tsx b/packages/danmaku-anywhere/src/content/controller/ui/floatingPanel/pages/DebugPage.tsx index 1f3273c2..bb6c690f 100644 --- a/packages/danmaku-anywhere/src/content/controller/ui/floatingPanel/pages/DebugPage.tsx +++ b/packages/danmaku-anywhere/src/content/controller/ui/floatingPanel/pages/DebugPage.tsx @@ -1,40 +1,397 @@ -import { Divider } from '@mui/material' +import { ContentCopy } from '@mui/icons-material' +import { + alpha, + Box, + Chip, + Divider, + IconButton, + Stack, + styled, + Tab, + Table, + TableBody, + TableCell, + TableRow, + Tabs, + Tooltip, + Typography, +} from '@mui/material' import { produce } from 'immer' +import { type ReactNode, useState } from 'react' import { useDialogStore } from '@/common/components/Dialog/dialogStore' -import { ScrollBox } from '@/common/components/layout/ScrollBox' +import { TabLayout } from '@/common/components/layout/TabLayout' +import { TabToolbar } from '@/common/components/layout/TabToolbar' import { useToast } from '@/common/components/Toast/toastStore' import { useExtensionOptions } from '@/common/options/extensionOptions/useExtensionOptions' +import type { FrameState } from '@/content/controller/store/store' import { useStore } from '@/content/controller/store/store' -export const DebugPage = () => { - const state = useStore() +// --- Helpers --- + +const StatusDot = styled('span')<{ active: boolean }>(({ theme, active }) => ({ + display: 'inline-block', + width: 8, + height: 8, + borderRadius: '50%', + backgroundColor: active + ? theme.palette.success.main + : theme.palette.action.disabled, + flexShrink: 0, +})) + +const BoolChip = ({ label, value }: { label: string; value: boolean }) => ( + +) + +const FieldRow = ({ label, value }: { label: string; value: ReactNode }) => ( + + + {label} + + {value} + +) + +const FieldTable = ({ children }: { children: ReactNode }) => ( + + {children} +
+) + +const SectionHeader = ({ children }: { children: ReactNode }) => ( + + {children} + +) + +// --- Frame Card --- + +const FrameCard = styled(Box, { + shouldForwardProp: (prop) => prop !== 'isActive', +})<{ isActive: boolean }>(({ theme, isActive }) => ({ + padding: theme.spacing(1, 1.5), + borderRadius: theme.shape.borderRadius, + cursor: 'pointer', + border: `1px solid ${isActive ? theme.palette.primary.main : theme.palette.divider}`, + backgroundColor: isActive + ? alpha(theme.palette.primary.main, 0.08) + : 'transparent', + transition: 'all 0.15s ease', + '&:hover': { + backgroundColor: isActive + ? alpha(theme.palette.primary.main, 0.12) + : theme.palette.action.hover, + }, +})) + +const FrameItem = ({ + frame, + isActive, + onSelect, +}: { + frame: FrameState + isActive: boolean + onSelect: () => void +}) => ( + + + + + Frame #{frame.frameId} + + {isActive && ( + + )} + + + {frame.url} + + + + + + + +) + +// --- Tab Panels --- + +const FramesPanel = () => { + const { allFrames, activeFrame, setActiveFrame } = useStore.use.frame() + const frames = Array.from(allFrames.values()) + + return ( + + Frames ({frames.length}) + {frames.length === 0 ? ( + + No frames detected + + ) : ( + + {frames.map((frame) => ( + setActiveFrame(frame.frameId)} + /> + ))} + + )} + + ) +} + +const StatePanel = () => { + const danmaku = useStore.use.danmaku() + const integration = useStore.use.integration() + const integrationForm = useStore.use.integrationForm() + const isDisconnected = useStore.use.isDisconnected() + const videoId = useStore.use.videoId?.() const toastState = useToast() - const { data } = useExtensionOptions() const { dialogs, closingIds, loadingIds } = useDialogStore() - // biome-ignore lint/suspicious/noExplicitAny: debug page does not need strict typing - const displayState = produce(state, (draft: any) => { - delete draft.danmaku.comments - if (draft.danmaku.episodes) { - for (const item of draft.danmaku.episodes) { - if ('comments' in item) { - delete item.comments + return ( + + General + + + } + /> + + {videoId ?? 'none'} + + } + /> + + + + + Danmaku + + + + + + + } + /> + + + + {danmaku.filter || '(empty)'} + + } + /> + + + + + Integration + + + + + + } + /> + {integration.errorMessage && ( + + {integration.errorMessage} + + } + /> + )} + {integration.mediaInfo && ( + + )} + + + + + Integration Form + + + + + + + } + /> + + + + + Toast / Dialogs + + {JSON.stringify(toastState, null, 2)} + {'\n'} + {JSON.stringify({ dialogs, closingIds, loadingIds }, null, 2)} + + + ) +} + +const OptionsPanel = () => { + const { data: options } = useExtensionOptions() + + return ( + + {JSON.stringify(options, null, 2)} + + ) +} + +// --- Main --- + +enum DebugTab { + Frames = 0, + State = 1, + Options = 2, +} + +export const DebugPage = () => { + const [tab, setTab] = useState(DebugTab.Frames) + const [copied, setCopied] = useState(false) + const state = useStore() + const { data: options } = useExtensionOptions() + + const handleCopyState = () => { + // biome-ignore lint/suspicious/noExplicitAny: debug page serialization + const snapshot = produce(state, (draft: any) => { + delete draft.danmaku.comments + if (draft.danmaku.episodes) { + for (const item of draft.danmaku.episodes) { + if ('comments' in item) { + delete item.comments + } } } - } - draft.frame.allFrames = Object.fromEntries(draft.frame.allFrames.entries()) - draft.options = data - }) + draft.frame.allFrames = Object.fromEntries( + draft.frame.allFrames.entries() + ) + draft.options = options + }) + void navigator.clipboard.writeText(JSON.stringify(snapshot, null, 2)) + setCopied(true) + setTimeout(() => setCopied(false), 1500) + } return ( - -
-        {JSON.stringify(displayState, null, 2)}
-        
-        {JSON.stringify(toastState, null, 2)}
-        
-        {JSON.stringify({ dialogs, closingIds, loadingIds }, null, 2)}
-      
-
+ + + + + + + + + + setTab(v)} + variant="fullWidth" + sx={{ + minHeight: 36, + '& .MuiTab-root': { minHeight: 36, fontSize: 12, py: 0 }, + }} + > + + + + + + + + {tab === DebugTab.Frames && } + {tab === DebugTab.State && } + {tab === DebugTab.Options && } + + ) } From 09f93f42fc4d5318b1f82db6e33058c5d4bb83d2 Mon Sep 17 00:00:00 2001 From: Mr-Quin <8700123+Mr-Quin@users.noreply.github.com> Date: Tue, 17 Mar 2026 20:47:56 -0700 Subject: [PATCH 2/4] ddas --- app/web/src/app/app.routes.ts | 10 +- .../debug/iframe-debug-page.component.ts | 20 - .../cross-origin-iframe-panel.component.ts | 80 ++++ .../panels/debug-panel.component.ts | 36 ++ .../panels/native-video-panel.component.ts | 72 ++++ .../same-origin-iframe-panel.component.ts | 33 ++ .../playground/playground-page.component.ts | 126 ++++++ .../local/services/local-player.service.ts | 2 + .../src/app/features/local/util/file-tree.ts | 26 +- .../components/sidebar/sidebar.component.ts | 6 +- .../src/background/rpc/RpcManager.ts | 3 + .../src/common/rpcClient/background/types.ts | 10 +- .../common/standalone/standaloneHandlers.ts | 1 + .../controller/danmaku/frame/FrameManager.tsx | 31 +- .../src/content/controller/store/store.ts | 13 + .../ui/floatingPanel/pages/DebugPage.tsx | 397 ------------------ .../floatingPanel/pages/debug/DebugPage.tsx | 96 +++++ .../pages/debug/components/DebugShared.tsx | 83 ++++ .../pages/debug/components/FramesPanel.tsx | 273 ++++++++++++ .../pages/debug/components/OptionsPanel.tsx | 39 ++ .../pages/debug/components/StatePanel.tsx | 76 ++++ .../ui/floatingPanel/pages/debug/index.ts | 1 + .../content/controller/ui/router/routes.tsx | 2 +- .../player/PlayerCommandHandler.service.ts | 10 +- .../player/videoSkip/VideoSkip.service.ts | 12 +- 25 files changed, 1005 insertions(+), 453 deletions(-) delete mode 100644 app/web/src/app/features/debug/iframe-debug-page.component.ts create mode 100644 app/web/src/app/features/debug/playground/panels/cross-origin-iframe-panel.component.ts create mode 100644 app/web/src/app/features/debug/playground/panels/debug-panel.component.ts create mode 100644 app/web/src/app/features/debug/playground/panels/native-video-panel.component.ts create mode 100644 app/web/src/app/features/debug/playground/panels/same-origin-iframe-panel.component.ts create mode 100644 app/web/src/app/features/debug/playground/playground-page.component.ts delete mode 100644 packages/danmaku-anywhere/src/content/controller/ui/floatingPanel/pages/DebugPage.tsx create mode 100644 packages/danmaku-anywhere/src/content/controller/ui/floatingPanel/pages/debug/DebugPage.tsx create mode 100644 packages/danmaku-anywhere/src/content/controller/ui/floatingPanel/pages/debug/components/DebugShared.tsx create mode 100644 packages/danmaku-anywhere/src/content/controller/ui/floatingPanel/pages/debug/components/FramesPanel.tsx create mode 100644 packages/danmaku-anywhere/src/content/controller/ui/floatingPanel/pages/debug/components/OptionsPanel.tsx create mode 100644 packages/danmaku-anywhere/src/content/controller/ui/floatingPanel/pages/debug/components/StatePanel.tsx create mode 100644 packages/danmaku-anywhere/src/content/controller/ui/floatingPanel/pages/debug/index.ts diff --git a/app/web/src/app/app.routes.ts b/app/web/src/app/app.routes.ts index ecaf9260..6b8924ad 100644 --- a/app/web/src/app/app.routes.ts +++ b/app/web/src/app/app.routes.ts @@ -107,13 +107,13 @@ export const routes: Routes = [ title: `Debug Components | ${PAGE_TITLE}`, }, { - path: 'iframe', + path: 'playground', loadComponent: () => - import('./features/debug/iframe-debug-page.component').then( - (m) => m.IframeDebugPageComponent - ), + import( + './features/debug/playground/playground-page.component' + ).then((m) => m.PlaygroundPageComponent), canActivate: [developmentOnly], - title: `Debug iframe | ${PAGE_TITLE}`, + title: `Playground | ${PAGE_TITLE}`, }, ], }, diff --git a/app/web/src/app/features/debug/iframe-debug-page.component.ts b/app/web/src/app/features/debug/iframe-debug-page.component.ts deleted file mode 100644 index c9de7196..00000000 --- a/app/web/src/app/features/debug/iframe-debug-page.component.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { ChangeDetectionStrategy, Component, inject } from '@angular/core' -import { DomSanitizer } from '@angular/platform-browser' - -@Component({ - selector: 'da-iframe-debug-page', - changeDetection: ChangeDetectionStrategy.OnPush, - template: ` -
- -
- `, -}) -export class IframeDebugPageComponent { - private readonly domSanitizer = inject(DomSanitizer) - protected readonly iframeSrc = - this.domSanitizer.bypassSecurityTrustResourceUrl(window.location.origin) -} diff --git a/app/web/src/app/features/debug/playground/panels/cross-origin-iframe-panel.component.ts b/app/web/src/app/features/debug/playground/panels/cross-origin-iframe-panel.component.ts new file mode 100644 index 00000000..1fdfc318 --- /dev/null +++ b/app/web/src/app/features/debug/playground/panels/cross-origin-iframe-panel.component.ts @@ -0,0 +1,80 @@ +import { + ChangeDetectionStrategy, + Component, + computed, + inject, + output, + signal, +} from '@angular/core' +import { FormsModule } from '@angular/forms' +import { DomSanitizer } from '@angular/platform-browser' +import { InputText } from 'primeng/inputtext' +import { Select } from 'primeng/select' + +import { DebugPanelComponent } from './debug-panel.component' + +interface UrlPreset { + label: string + url: string +} + +@Component({ + selector: 'da-cross-origin-iframe-panel', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [DebugPanelComponent, Select, InputText, FormsModule], + template: ` + +
+ + +
+ +
+ `, +}) +export class CrossOriginIframePanelComponent { + private readonly domSanitizer = inject(DomSanitizer) + + protected readonly presets: UrlPreset[] = [ + { + label: 'YouTube Embed', + url: 'https://www.youtube.com/embed/dQw4w9WgXcQ', + }, + { + label: 'Vimeo Embed', + url: 'https://player.vimeo.com/video/148751763', + }, + { + label: 'Wikipedia', + url: 'https://en.wikipedia.org/wiki/Main_Page', + }, + ] + + protected readonly $url = signal(this.presets[0].url) + + protected readonly $sanitizedUrl = computed(() => + this.domSanitizer.bypassSecurityTrustResourceUrl(this.$url()) + ) + + readonly remove = output() +} diff --git a/app/web/src/app/features/debug/playground/panels/debug-panel.component.ts b/app/web/src/app/features/debug/playground/panels/debug-panel.component.ts new file mode 100644 index 00000000..c39ae1bf --- /dev/null +++ b/app/web/src/app/features/debug/playground/panels/debug-panel.component.ts @@ -0,0 +1,36 @@ +import { + ChangeDetectionStrategy, + Component, + input, + output, +} from '@angular/core' +import { Button } from 'primeng/button' + +@Component({ + selector: 'da-debug-panel', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [Button], + template: ` +
+
+ {{ title() }} + +
+ +
+ +
+
+ `, +}) +export class DebugPanelComponent { + readonly title = input.required() + readonly remove = output() +} diff --git a/app/web/src/app/features/debug/playground/panels/native-video-panel.component.ts b/app/web/src/app/features/debug/playground/panels/native-video-panel.component.ts new file mode 100644 index 00000000..51838158 --- /dev/null +++ b/app/web/src/app/features/debug/playground/panels/native-video-panel.component.ts @@ -0,0 +1,72 @@ +import { + ChangeDetectionStrategy, + Component, + output, + signal, +} from '@angular/core' +import { FormsModule } from '@angular/forms' +import { Select } from 'primeng/select' + +import { DebugPanelComponent } from './debug-panel.component' + +interface VideoOption { + label: string + url: string +} + +@Component({ + selector: 'da-native-video-panel', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [DebugPanelComponent, Select, FormsModule], + template: ` + +
+ +
+
+ +
+
+ `, +}) +export class NativeVideoPanelComponent { + protected readonly videoOptions: VideoOption[] = [ + { + label: 'Big Buck Bunny', + url: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4', + }, + { + label: 'Elephants Dream', + url: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4', + }, + { + label: 'For Bigger Blazes', + url: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4', + }, + { + label: 'Sintel', + url: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/Sintel.mp4', + }, + { + label: 'Tears of Steel', + url: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/TearsOfSteel.mp4', + }, + ] + + protected readonly $videoUrl = signal(this.videoOptions[0].url) + + readonly remove = output() +} diff --git a/app/web/src/app/features/debug/playground/panels/same-origin-iframe-panel.component.ts b/app/web/src/app/features/debug/playground/panels/same-origin-iframe-panel.component.ts new file mode 100644 index 00000000..3d0baf9a --- /dev/null +++ b/app/web/src/app/features/debug/playground/panels/same-origin-iframe-panel.component.ts @@ -0,0 +1,33 @@ +import { + ChangeDetectionStrategy, + Component, + inject, + output, +} from '@angular/core' +import { DomSanitizer } from '@angular/platform-browser' + +import { DebugPanelComponent } from './debug-panel.component' + +@Component({ + selector: 'da-same-origin-iframe-panel', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [DebugPanelComponent], + template: ` + + + + `, +}) +export class SameOriginIframePanelComponent { + private readonly domSanitizer = inject(DomSanitizer) + + protected readonly iframeSrc = + this.domSanitizer.bypassSecurityTrustResourceUrl( + `${window.location.origin}/local` + ) + + readonly remove = output() +} diff --git a/app/web/src/app/features/debug/playground/playground-page.component.ts b/app/web/src/app/features/debug/playground/playground-page.component.ts new file mode 100644 index 00000000..899be153 --- /dev/null +++ b/app/web/src/app/features/debug/playground/playground-page.component.ts @@ -0,0 +1,126 @@ +import { ChangeDetectionStrategy, Component, signal } from '@angular/core' +import { FormsModule } from '@angular/forms' +import { Button } from 'primeng/button' +import { Select } from 'primeng/select' + +import { CrossOriginIframePanelComponent } from './panels/cross-origin-iframe-panel.component' +import { NativeVideoPanelComponent } from './panels/native-video-panel.component' +import { SameOriginIframePanelComponent } from './panels/same-origin-iframe-panel.component' + +type PanelType = 'same-origin-iframe' | 'cross-origin-iframe' | 'native-video' + +interface DebugPanel { + id: string + type: PanelType +} + +interface PanelTypeOption { + label: string + value: PanelType +} + +@Component({ + selector: 'da-playground-page', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + Button, + Select, + FormsModule, + SameOriginIframePanelComponent, + CrossOriginIframePanelComponent, + NativeVideoPanelComponent, + ], + template: ` +
+
+

Playground

+
+ + + @if ($panels().length > 0) { + + } +
+
+ + @if ($panels().length === 0) { +
+

Add a panel to get started

+
+ } @else { +
+ @for (panel of $panels(); track panel.id) { +
+ @switch (panel.type) { + @case ('same-origin-iframe') { + + } + @case ('cross-origin-iframe') { + + } + @case ('native-video') { + + } + } +
+ } +
+ } +
+ `, +}) +export class PlaygroundPageComponent { + protected readonly panelTypeOptions: PanelTypeOption[] = [ + { label: 'Same-Origin Iframe', value: 'same-origin-iframe' }, + { label: 'Cross-Origin Iframe', value: 'cross-origin-iframe' }, + { label: 'Native Video', value: 'native-video' }, + ] + + protected readonly $selectedType = signal('same-origin-iframe') + protected readonly $panels = signal([ + { id: crypto.randomUUID(), type: 'native-video' }, + ]) + + protected addPanel() { + const panel: DebugPanel = { + id: crypto.randomUUID(), + type: this.$selectedType(), + } + this.$panels.update((panels) => [...panels, panel]) + } + + protected removePanel(id: string) { + this.$panels.update((panels) => panels.filter((p) => p.id !== id)) + } + + protected clearAll() { + this.$panels.set([]) + } +} diff --git a/app/web/src/app/features/local/services/local-player.service.ts b/app/web/src/app/features/local/services/local-player.service.ts index 1599f558..8ec1790d 100644 --- a/app/web/src/app/features/local/services/local-player.service.ts +++ b/app/web/src/app/features/local/services/local-player.service.ts @@ -76,10 +76,12 @@ export class LocalPlayerService { expandAll() { this.fileTree.expandAll() + this.bumpTree() } collapseAll() { this.fileTree.collapseAll() + this.bumpTree() } async loadNode(node: FileTreeNode) { diff --git a/app/web/src/app/features/local/util/file-tree.ts b/app/web/src/app/features/local/util/file-tree.ts index c783ac85..91732c33 100644 --- a/app/web/src/app/features/local/util/file-tree.ts +++ b/app/web/src/app/features/local/util/file-tree.ts @@ -97,17 +97,19 @@ function pruneEmpty(node: FileTreeNode): void { }) } -function setExpanded(nodes: TreeNode[], expanded: boolean): TreeNode[] { - nodes.forEach((n) => { +function cloneWithExpanded(nodes: TreeNode[], expanded: boolean): TreeNode[] { + return nodes.map((n) => { if (n.type === 'directory' || n.type === 'removableDirectory') { - n.expanded = expanded - if (n.children) - n.children.forEach((c) => { - setExpanded([c], expanded) - }) + return { + ...n, + expanded, + children: n.children + ? cloneWithExpanded(n.children, expanded) + : undefined, + } } + return { ...n } }) - return nodes } export class FileTree { @@ -288,7 +290,7 @@ export class FileTree { } getNodes(): FileTreeNode[] { - return this.roots + return [...this.roots] } getInfo(node: FileTreeNode): TreeNodeInfo { @@ -321,11 +323,13 @@ export class FileTree { } expandAll() { - setExpanded(this.roots, true) + this.roots = cloneWithExpanded(this.roots, true) + this.buildIndexes() } collapseAll() { - setExpanded(this.roots, false) + this.roots = cloneWithExpanded(this.roots, false) + this.buildIndexes() } private buildIndexes() { diff --git a/app/web/src/app/layout/components/sidebar/sidebar.component.ts b/app/web/src/app/layout/components/sidebar/sidebar.component.ts index 5a6b3af5..c9f5b3b3 100644 --- a/app/web/src/app/layout/components/sidebar/sidebar.component.ts +++ b/app/web/src/app/layout/components/sidebar/sidebar.component.ts @@ -175,9 +175,9 @@ export class AppSidebar { icon: 'toolbar', }, { - path: '/debug/iframe', - label: 'iframe Debug', - icon: 'pip', + path: '/debug/playground', + label: 'Playground', + icon: 'science', }, ], }, diff --git a/packages/danmaku-anywhere/src/background/rpc/RpcManager.ts b/packages/danmaku-anywhere/src/background/rpc/RpcManager.ts index c31b74df..e193942c 100644 --- a/packages/danmaku-anywhere/src/background/rpc/RpcManager.ts +++ b/packages/danmaku-anywhere/src/background/rpc/RpcManager.ts @@ -392,6 +392,9 @@ export class RpcManager { 'relay:command:controllerReady': passThrough( relayFrameClient['relay:command:controllerReady'] ), + 'relay:command:debugSkipButton': passThrough( + relayFrameClient['relay:command:debugSkipButton'] + ), 'relay:event:playerReady': passThrough( relayFrameClient['relay:event:playerReady'] ), diff --git a/packages/danmaku-anywhere/src/common/rpcClient/background/types.ts b/packages/danmaku-anywhere/src/common/rpcClient/background/types.ts index 061c93d8..01ec506c 100644 --- a/packages/danmaku-anywhere/src/common/rpcClient/background/types.ts +++ b/packages/danmaku-anywhere/src/common/rpcClient/background/types.ts @@ -181,6 +181,11 @@ export type PlayerRelayCommands = { void, FrameContext > + 'relay:command:debugSkipButton': RPCDef< + InputWithFrameId, + void, + FrameContext + > } type PlayerReadyData = { @@ -193,7 +198,10 @@ type PlayerReadyData = { export type PlayerRelayEvents = { 'relay:event:playerReady': RPCDef, void> 'relay:event:playerUnload': RPCDef, void> - 'relay:event:videoChange': RPCDef, void> + 'relay:event:videoChange': RPCDef< + InputWithFrameId<{ src: string; width: number; height: number }>, + void + > 'relay:event:videoRemoved': RPCDef, void> 'relay:event:preloadNextEpisode': RPCDef, void> 'relay:event:showPopover': RPCDef, void> diff --git a/packages/danmaku-anywhere/src/common/standalone/standaloneHandlers.ts b/packages/danmaku-anywhere/src/common/standalone/standaloneHandlers.ts index 9f192e5b..a8a735a7 100644 --- a/packages/danmaku-anywhere/src/common/standalone/standaloneHandlers.ts +++ b/packages/danmaku-anywhere/src/common/standalone/standaloneHandlers.ts @@ -162,6 +162,7 @@ export const standalonePlayerCommandHandlers: StandaloneRpcHandlers undefined, 'relay:command:show': () => undefined, 'relay:command:controllerReady': () => undefined, + 'relay:command:debugSkipButton': () => undefined, } export const standalonePlayerEventHandlers: StandaloneRpcHandlers = diff --git a/packages/danmaku-anywhere/src/content/controller/danmaku/frame/FrameManager.tsx b/packages/danmaku-anywhere/src/content/controller/danmaku/frame/FrameManager.tsx index eb6f57ca..d593a3cb 100644 --- a/packages/danmaku-anywhere/src/content/controller/danmaku/frame/FrameManager.tsx +++ b/packages/danmaku-anywhere/src/content/controller/danmaku/frame/FrameManager.tsx @@ -42,19 +42,24 @@ export const FrameManager = () => { useMigrateDanmaku() - const videoChangeHandler = useEventCallback((frameId: number) => { - setVideoId(`${frameId}-${Date.now()}`) + const videoChangeHandler = useEventCallback( + (frameId: number, data: { src: string; width: number; height: number }) => { + setVideoId(`${frameId}-${Date.now()}`) + + if (activeFrame?.hasVideo && activeFrame.frameId !== frameId) { + toast.warn( + t( + 'danmaku.alert.multipleFrames', + 'Multiple frames with video detected' + ) + ) + return + } - if (activeFrame?.hasVideo && activeFrame.frameId !== frameId) { - toast.warn( - t('danmaku.alert.multipleFrames', 'Multiple frames with video detected') - ) - return + updateFrame(frameId, { hasVideo: true, videoInfo: data }) + useStore.getState().frame.setActiveFrame(frameId) } - - updateFrame(frameId, { hasVideo: true }) - useStore.getState().frame.setActiveFrame(frameId) - }) + ) const videoRemovedHandler = useEventCallback((frameId: number) => { if (activeFrame?.frameId === frameId) { @@ -97,8 +102,8 @@ export const FrameManager = () => { 'relay:event:playerUnload': async ({ frameId }) => { frameRegistry.unregisterFrame(frameId) }, - 'relay:event:videoChange': async ({ frameId }) => { - videoChangeHandler(frameId) + 'relay:event:videoChange': async ({ frameId, data }) => { + videoChangeHandler(frameId, data) }, 'relay:event:videoRemoved': async ({ frameId }) => { videoRemovedHandler(frameId) diff --git a/packages/danmaku-anywhere/src/content/controller/store/store.ts b/packages/danmaku-anywhere/src/content/controller/store/store.ts index c934cb04..11d7552c 100644 --- a/packages/danmaku-anywhere/src/content/controller/store/store.ts +++ b/packages/danmaku-anywhere/src/content/controller/store/store.ts @@ -21,6 +21,12 @@ export interface FrameState { mounted: boolean // Whether a video element is detected in this frame hasVideo: boolean + // Info about the active video element + videoInfo?: { + src: string + width: number + height: number + } } interface StoreState { @@ -67,6 +73,7 @@ interface StoreState { } seekToTime: (time: number) => void + debugShowSkipButton: (frameId: number) => void /** * Media information for pages with integration @@ -197,6 +204,12 @@ const useStoreBase = create()( }) }, + debugShowSkipButton: (frameId) => { + void playerRpcClient.player['relay:command:debugSkipButton']({ + frameId, + }) + }, + integration: { active: false, activate: () => diff --git a/packages/danmaku-anywhere/src/content/controller/ui/floatingPanel/pages/DebugPage.tsx b/packages/danmaku-anywhere/src/content/controller/ui/floatingPanel/pages/DebugPage.tsx deleted file mode 100644 index bb6c690f..00000000 --- a/packages/danmaku-anywhere/src/content/controller/ui/floatingPanel/pages/DebugPage.tsx +++ /dev/null @@ -1,397 +0,0 @@ -import { ContentCopy } from '@mui/icons-material' -import { - alpha, - Box, - Chip, - Divider, - IconButton, - Stack, - styled, - Tab, - Table, - TableBody, - TableCell, - TableRow, - Tabs, - Tooltip, - Typography, -} from '@mui/material' -import { produce } from 'immer' -import { type ReactNode, useState } from 'react' -import { useDialogStore } from '@/common/components/Dialog/dialogStore' -import { TabLayout } from '@/common/components/layout/TabLayout' -import { TabToolbar } from '@/common/components/layout/TabToolbar' -import { useToast } from '@/common/components/Toast/toastStore' -import { useExtensionOptions } from '@/common/options/extensionOptions/useExtensionOptions' -import type { FrameState } from '@/content/controller/store/store' -import { useStore } from '@/content/controller/store/store' - -// --- Helpers --- - -const StatusDot = styled('span')<{ active: boolean }>(({ theme, active }) => ({ - display: 'inline-block', - width: 8, - height: 8, - borderRadius: '50%', - backgroundColor: active - ? theme.palette.success.main - : theme.palette.action.disabled, - flexShrink: 0, -})) - -const BoolChip = ({ label, value }: { label: string; value: boolean }) => ( - -) - -const FieldRow = ({ label, value }: { label: string; value: ReactNode }) => ( - - - {label} - - {value} - -) - -const FieldTable = ({ children }: { children: ReactNode }) => ( - - {children} -
-) - -const SectionHeader = ({ children }: { children: ReactNode }) => ( - - {children} - -) - -// --- Frame Card --- - -const FrameCard = styled(Box, { - shouldForwardProp: (prop) => prop !== 'isActive', -})<{ isActive: boolean }>(({ theme, isActive }) => ({ - padding: theme.spacing(1, 1.5), - borderRadius: theme.shape.borderRadius, - cursor: 'pointer', - border: `1px solid ${isActive ? theme.palette.primary.main : theme.palette.divider}`, - backgroundColor: isActive - ? alpha(theme.palette.primary.main, 0.08) - : 'transparent', - transition: 'all 0.15s ease', - '&:hover': { - backgroundColor: isActive - ? alpha(theme.palette.primary.main, 0.12) - : theme.palette.action.hover, - }, -})) - -const FrameItem = ({ - frame, - isActive, - onSelect, -}: { - frame: FrameState - isActive: boolean - onSelect: () => void -}) => ( - - - - - Frame #{frame.frameId} - - {isActive && ( - - )} - - - {frame.url} - - - - - - - -) - -// --- Tab Panels --- - -const FramesPanel = () => { - const { allFrames, activeFrame, setActiveFrame } = useStore.use.frame() - const frames = Array.from(allFrames.values()) - - return ( - - Frames ({frames.length}) - {frames.length === 0 ? ( - - No frames detected - - ) : ( - - {frames.map((frame) => ( - setActiveFrame(frame.frameId)} - /> - ))} - - )} - - ) -} - -const StatePanel = () => { - const danmaku = useStore.use.danmaku() - const integration = useStore.use.integration() - const integrationForm = useStore.use.integrationForm() - const isDisconnected = useStore.use.isDisconnected() - const videoId = useStore.use.videoId?.() - const toastState = useToast() - const { dialogs, closingIds, loadingIds } = useDialogStore() - - return ( - - General - - - } - /> - - {videoId ?? 'none'} - - } - /> - - - - - Danmaku - - - - - - - } - /> - - - - {danmaku.filter || '(empty)'} - - } - /> - - - - - Integration - - - - - - } - /> - {integration.errorMessage && ( - - {integration.errorMessage} - - } - /> - )} - {integration.mediaInfo && ( - - )} - - - - - Integration Form - - - - - - - } - /> - - - - - Toast / Dialogs - - {JSON.stringify(toastState, null, 2)} - {'\n'} - {JSON.stringify({ dialogs, closingIds, loadingIds }, null, 2)} - - - ) -} - -const OptionsPanel = () => { - const { data: options } = useExtensionOptions() - - return ( - - {JSON.stringify(options, null, 2)} - - ) -} - -// --- Main --- - -enum DebugTab { - Frames = 0, - State = 1, - Options = 2, -} - -export const DebugPage = () => { - const [tab, setTab] = useState(DebugTab.Frames) - const [copied, setCopied] = useState(false) - const state = useStore() - const { data: options } = useExtensionOptions() - - const handleCopyState = () => { - // biome-ignore lint/suspicious/noExplicitAny: debug page serialization - const snapshot = produce(state, (draft: any) => { - delete draft.danmaku.comments - if (draft.danmaku.episodes) { - for (const item of draft.danmaku.episodes) { - if ('comments' in item) { - delete item.comments - } - } - } - draft.frame.allFrames = Object.fromEntries( - draft.frame.allFrames.entries() - ) - draft.options = options - }) - void navigator.clipboard.writeText(JSON.stringify(snapshot, null, 2)) - setCopied(true) - setTimeout(() => setCopied(false), 1500) - } - - return ( - - - - - - - - - - setTab(v)} - variant="fullWidth" - sx={{ - minHeight: 36, - '& .MuiTab-root': { minHeight: 36, fontSize: 12, py: 0 }, - }} - > - - - - - - - - {tab === DebugTab.Frames && } - {tab === DebugTab.State && } - {tab === DebugTab.Options && } - - - ) -} diff --git a/packages/danmaku-anywhere/src/content/controller/ui/floatingPanel/pages/debug/DebugPage.tsx b/packages/danmaku-anywhere/src/content/controller/ui/floatingPanel/pages/debug/DebugPage.tsx new file mode 100644 index 00000000..71e948bb --- /dev/null +++ b/packages/danmaku-anywhere/src/content/controller/ui/floatingPanel/pages/debug/DebugPage.tsx @@ -0,0 +1,96 @@ +import { ContentCopy } from '@mui/icons-material' +import { Box, Divider, IconButton, Tab, Tabs, Tooltip } from '@mui/material' +import { produce } from 'immer' +import { useState } from 'react' +import { TabLayout } from '@/common/components/layout/TabLayout' +import { TabToolbar } from '@/common/components/layout/TabToolbar' +import { useExtensionOptions } from '@/common/options/extensionOptions/useExtensionOptions' +import { useStore } from '@/content/controller/store/store' + +import { FramesPanel } from './components/FramesPanel' +import { OptionsPanel } from './components/OptionsPanel' +import { StatePanel } from './components/StatePanel' + +enum DebugTab { + Frames = 0, + State = 1, + Options = 2, +} + +export const DebugPage = () => { + const [tab, setTab] = useState(DebugTab.Frames) + const [copied, setCopied] = useState(false) + const state = useStore() + const { data: options } = useExtensionOptions() + + const handleCopyState = () => { + // biome-ignore lint/suspicious/noExplicitAny: debug page serialization + const snapshot = produce(state, (draft: any) => { + delete draft.danmaku.comments + if (draft.danmaku.episodes) { + for (const item of draft.danmaku.episodes) { + if ('comments' in item) { + delete item.comments + } + } + } + draft.frame.allFrames = Object.fromEntries( + draft.frame.allFrames.entries() + ) + draft.options = options + }) + void navigator.clipboard.writeText(JSON.stringify(snapshot, null, 2)) + setCopied(true) + setTimeout(() => setCopied(false), 1500) + } + + return ( + + + + + + + + + + + + setTab(v)} + variant="fullWidth" + sx={{ + borderBottom: 1, + borderColor: 'divider', + minHeight: 36, + '& .MuiTab-root': { + minHeight: 36, + fontSize: 12, + textTransform: 'none', + py: 0, + }, + }} + > + + + + + + + {tab === DebugTab.Frames && } + {tab === DebugTab.State && ( + + + + )} + {tab === DebugTab.Options && ( + + + + )} + + + + ) +} diff --git a/packages/danmaku-anywhere/src/content/controller/ui/floatingPanel/pages/debug/components/DebugShared.tsx b/packages/danmaku-anywhere/src/content/controller/ui/floatingPanel/pages/debug/components/DebugShared.tsx new file mode 100644 index 00000000..e1b743d1 --- /dev/null +++ b/packages/danmaku-anywhere/src/content/controller/ui/floatingPanel/pages/debug/components/DebugShared.tsx @@ -0,0 +1,83 @@ +import { + Chip, + styled, + Table, + TableBody, + TableCell, + TableRow, + Typography, +} from '@mui/material' +import type { ReactNode } from 'react' + +export const StatusDot = styled('span')<{ active: boolean }>( + ({ theme, active }) => ({ + display: 'inline-block', + width: 8, + height: 8, + borderRadius: '50%', + backgroundColor: active + ? theme.palette.success.main + : theme.palette.action.disabled, + flexShrink: 0, + }) +) + +export const BoolChip = ({ + label, + value, +}: { + label: string + value: boolean +}) => ( + +) + +export const FieldRow = ({ + label, + value, +}: { + label: string + value: ReactNode +}) => ( + + + {label} + + {value} + +) + +export const FieldTable = ({ children }: { children: ReactNode }) => ( + + {children} +
+) + +export const SectionHeader = ({ children }: { children: ReactNode }) => ( + + {children} + +) diff --git a/packages/danmaku-anywhere/src/content/controller/ui/floatingPanel/pages/debug/components/FramesPanel.tsx b/packages/danmaku-anywhere/src/content/controller/ui/floatingPanel/pages/debug/components/FramesPanel.tsx new file mode 100644 index 00000000..3d7a2bb5 --- /dev/null +++ b/packages/danmaku-anywhere/src/content/controller/ui/floatingPanel/pages/debug/components/FramesPanel.tsx @@ -0,0 +1,273 @@ +import { + BugReport as BugReportIcon, + Extension as ExtensionIcon, + KeyboardArrowDown, + KeyboardArrowRight, + PlayCircleOutline as PlayIcon, + Videocam as VideocamIcon, +} from '@mui/icons-material' +import { + alpha, + Box, + Button, + Chip, + Collapse, + IconButton, + Stack, + styled, + Typography, +} from '@mui/material' +import { useState } from 'react' +import type { FrameState } from '@/content/controller/store/store' +import { useStore } from '@/content/controller/store/store' +import { StatusDot } from './DebugShared' + +const FrameCard = styled(Box, { + shouldForwardProp: (prop) => prop !== 'isActive', +})<{ isActive: boolean }>(({ theme, isActive }) => ({ + borderBottom: `1px solid ${theme.palette.divider}`, + backgroundColor: isActive + ? alpha(theme.palette.primary.main, 0.04) + : 'transparent', + transition: 'all 0.15s ease', + '&:hover': { + backgroundColor: isActive + ? alpha(theme.palette.primary.main, 0.08) + : theme.palette.action.hover, + }, +})) + +const FrameItem = ({ + frame, + isActive, + onSelect, +}: { + frame: FrameState + isActive: boolean + onSelect: () => void +}) => { + const [expanded, setExpanded] = useState(isActive) + const debugShowSkipButton = useStore.use.debugShowSkipButton() + + return ( + + {/* Header Row */} + setExpanded(!expanded)} + > + + {expanded ? ( + + ) : ( + + )} + + + + Frame #{frame.frameId} + + {isActive ? ( + + ) : ( + + )} + + + {/* Expanded Content */} + + + + {frame.url} + + + + + + + Started + + + + + + Mounted + + + + + + Has Video + + + + + {frame.videoInfo && ( + alpha(theme.palette.text.primary, 0.03)} + borderRadius={1} + border={(theme) => `1px solid ${theme.palette.divider}`} + > + + src:{' '} + {frame.videoInfo.src} + + + size:{' '} + {frame.videoInfo.width}x{frame.videoInfo.height} + + + )} + + {/* Frame Actions/Commands */} + {frame.hasVideo && ( + + + + )} + + + + ) +} + +export const FramesPanel = () => { + const { allFrames, activeFrame, setActiveFrame } = useStore.use.frame() + const frames = Array.from(allFrames.values()) + + return ( + + alpha(theme.palette.primary.main, 0.05)} + borderBottom={(theme) => `1px solid ${theme.palette.divider}`} + > + + Detected Frames ({frames.length}) + + + + {frames.length === 0 ? ( + + No frames detected + + ) : ( + + {frames.map((frame) => ( + setActiveFrame(frame.frameId)} + /> + ))} + + )} + + + ) +} diff --git a/packages/danmaku-anywhere/src/content/controller/ui/floatingPanel/pages/debug/components/OptionsPanel.tsx b/packages/danmaku-anywhere/src/content/controller/ui/floatingPanel/pages/debug/components/OptionsPanel.tsx new file mode 100644 index 00000000..2da11253 --- /dev/null +++ b/packages/danmaku-anywhere/src/content/controller/ui/floatingPanel/pages/debug/components/OptionsPanel.tsx @@ -0,0 +1,39 @@ +import { Box, Typography } from '@mui/material' +import { useExtensionOptions } from '@/common/options/extensionOptions/useExtensionOptions' + +export const OptionsPanel = () => { + const { data: options } = useExtensionOptions() + + return ( + + + + Extension Options + + + {JSON.stringify(options, null, 2)} + + + + ) +} diff --git a/packages/danmaku-anywhere/src/content/controller/ui/floatingPanel/pages/debug/components/StatePanel.tsx b/packages/danmaku-anywhere/src/content/controller/ui/floatingPanel/pages/debug/components/StatePanel.tsx new file mode 100644 index 00000000..f0cdfe04 --- /dev/null +++ b/packages/danmaku-anywhere/src/content/controller/ui/floatingPanel/pages/debug/components/StatePanel.tsx @@ -0,0 +1,76 @@ +import { Box, Typography } from '@mui/material' +import { useDialogStore } from '@/common/components/Dialog/dialogStore' +import { useToast } from '@/common/components/Toast/toastStore' +import { useStore } from '@/content/controller/store/store' + +// biome-ignore lint/suspicious/noExplicitAny: debug serialization +const JsonBlock = ({ title, data }: { title: string; data: any }) => ( + + + {title} + + + {JSON.stringify(data, null, 2)} + + +) + +export const StatePanel = () => { + const danmaku = useStore.use.danmaku() + const integration = useStore.use.integration() + const integrationForm = useStore.use.integrationForm() + const isDisconnected = useStore.use.isDisconnected() + const videoId = useStore.use.videoId?.() + const toastState = useToast() + const { dialogs, closingIds, loadingIds } = useDialogStore() + + // Build a nice serialized version of state + const generalState = { + isDisconnected, + videoId: videoId ?? null, + } + + const cleanDanmakuState = { + ...danmaku, + comments: `[Array(${danmaku.comments.length})]`, + episodes: danmaku.episodes ? `[Array(${danmaku.episodes.length})]` : null, + } + + return ( + + + + + + + + + ) +} diff --git a/packages/danmaku-anywhere/src/content/controller/ui/floatingPanel/pages/debug/index.ts b/packages/danmaku-anywhere/src/content/controller/ui/floatingPanel/pages/debug/index.ts new file mode 100644 index 00000000..1aff6828 --- /dev/null +++ b/packages/danmaku-anywhere/src/content/controller/ui/floatingPanel/pages/debug/index.ts @@ -0,0 +1 @@ +export { DebugPage } from './DebugPage' diff --git a/packages/danmaku-anywhere/src/content/controller/ui/router/routes.tsx b/packages/danmaku-anywhere/src/content/controller/ui/router/routes.tsx index 893fd507..33e7e65e 100644 --- a/packages/danmaku-anywhere/src/content/controller/ui/router/routes.tsx +++ b/packages/danmaku-anywhere/src/content/controller/ui/router/routes.tsx @@ -1,7 +1,7 @@ import { i18n } from '@/common/localization/i18n' import { PopupTab } from '@/content/controller/store/popupStore' import { CommentsPage } from '@/content/controller/ui/floatingPanel/pages/CommentsPage' -import { DebugPage } from '@/content/controller/ui/floatingPanel/pages/DebugPage' +import { DebugPage } from '@/content/controller/ui/floatingPanel/pages/debug' import { IntegrationPage } from '@/content/controller/ui/floatingPanel/pages/integrationPolicy/IntegrationPage' import { MountPage } from '@/content/controller/ui/floatingPanel/pages/mount/MountPage' import { SelectorPage } from '@/content/controller/ui/floatingPanel/pages/SelectorPage' diff --git a/packages/danmaku-anywhere/src/content/player/PlayerCommandHandler.service.ts b/packages/danmaku-anywhere/src/content/player/PlayerCommandHandler.service.ts index 375d7766..5858b130 100644 --- a/packages/danmaku-anywhere/src/content/player/PlayerCommandHandler.service.ts +++ b/packages/danmaku-anywhere/src/content/player/PlayerCommandHandler.service.ts @@ -77,9 +77,14 @@ export class PlayerCommandHandler { } private wireLifecycleEvents() { - this.videoNodeObs.addEventListener('videoNodeChange', () => { + this.videoNodeObs.addEventListener('videoNodeChange', (video) => { playerRpcClient.controller['relay:event:videoChange']({ frameId: this.frameId, + data: { + src: video.src || video.currentSrc, + width: video.clientWidth, + height: video.clientHeight, + }, }) }) @@ -180,6 +185,9 @@ export class PlayerCommandHandler { 'relay:command:enterPip': async () => { await this.enterPip() }, + 'relay:command:debugSkipButton': async () => { + this.videoSkip.debugShowSkipButton() + }, }, { logger: this.logger, diff --git a/packages/danmaku-anywhere/src/content/player/videoSkip/VideoSkip.service.ts b/packages/danmaku-anywhere/src/content/player/videoSkip/VideoSkip.service.ts index 04dea0b7..fdf43a53 100644 --- a/packages/danmaku-anywhere/src/content/player/videoSkip/VideoSkip.service.ts +++ b/packages/danmaku-anywhere/src/content/player/videoSkip/VideoSkip.service.ts @@ -6,7 +6,7 @@ import { type ILogger, LoggerSymbol } from '@/common/Logger' import { getTrackingService } from '@/common/telemetry/getTrackingService' import { SkipButton } from '@/content/player/components/SkipButton/SkipButton' import { DanmakuLayoutService } from '@/content/player/danmakuLayout/DanmakuLayout.service' -import type { SkipTarget } from '@/content/player/videoSkip/SkipTarget' +import { SkipTarget } from '@/content/player/videoSkip/SkipTarget' import { VideoEventService } from '../videoEvent/VideoEvent.service' import { parseCommentsForJumpTargets } from './videoSkipParser' @@ -64,6 +64,16 @@ export class VideoSkipService { this.currentVideo = null } + debugShowSkipButton() { + this.logger.debug('Showing debug skip button') + const time = this.currentVideo?.currentTime ?? 120 + const target = new SkipTarget({ + startTime: time, + endTime: time + 90, + }) + this.showSkipButton(target) + } + private setupEventListeners() { this.videoEventService.addVideoEventListener( 'timeupdate', From 208658b969e8129b7d7f1b82873ca156a3413350 Mon Sep 17 00:00:00 2001 From: Mr-Quin <8700123+Mr-Quin@users.noreply.github.com> Date: Tue, 17 Mar 2026 20:57:05 -0700 Subject: [PATCH 3/4] gsa --- .../features/debug/playground/panels/debug-panel.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/web/src/app/features/debug/playground/panels/debug-panel.component.ts b/app/web/src/app/features/debug/playground/panels/debug-panel.component.ts index c39ae1bf..85305b45 100644 --- a/app/web/src/app/features/debug/playground/panels/debug-panel.component.ts +++ b/app/web/src/app/features/debug/playground/panels/debug-panel.component.ts @@ -13,7 +13,7 @@ import { Button } from 'primeng/button' template: `
- {{ title() }} + {{ title() }} Date: Tue, 17 Mar 2026 21:34:42 -0700 Subject: [PATCH 4/4] sas --- .../panels/cross-origin-iframe-panel.component.ts | 15 ++++++++++++--- app/web/src/app/features/local/util/file-tree.ts | 2 +- .../ui/floatingPanel/pages/debug/DebugPage.tsx | 12 ++++++++---- .../content/player/videoSkip/VideoSkip.service.ts | 4 ++++ 4 files changed, 25 insertions(+), 8 deletions(-) diff --git a/app/web/src/app/features/debug/playground/panels/cross-origin-iframe-panel.component.ts b/app/web/src/app/features/debug/playground/panels/cross-origin-iframe-panel.component.ts index 1fdfc318..aff2b9fb 100644 --- a/app/web/src/app/features/debug/playground/panels/cross-origin-iframe-panel.component.ts +++ b/app/web/src/app/features/debug/playground/panels/cross-origin-iframe-panel.component.ts @@ -72,9 +72,18 @@ export class CrossOriginIframePanelComponent { protected readonly $url = signal(this.presets[0].url) - protected readonly $sanitizedUrl = computed(() => - this.domSanitizer.bypassSecurityTrustResourceUrl(this.$url()) - ) + protected readonly $sanitizedUrl = computed(() => { + const value = this.$url() + try { + const url = new URL(value) + if (url.protocol === 'http:' || url.protocol === 'https:') { + return this.domSanitizer.bypassSecurityTrustResourceUrl(value) + } + } catch { + // invalid URL + } + return this.domSanitizer.bypassSecurityTrustResourceUrl('about:blank') + }) readonly remove = output() } diff --git a/app/web/src/app/features/local/util/file-tree.ts b/app/web/src/app/features/local/util/file-tree.ts index 91732c33..9cda259c 100644 --- a/app/web/src/app/features/local/util/file-tree.ts +++ b/app/web/src/app/features/local/util/file-tree.ts @@ -108,7 +108,7 @@ function cloneWithExpanded(nodes: TreeNode[], expanded: boolean): TreeNode[] { : undefined, } } - return { ...n } + return n }) } diff --git a/packages/danmaku-anywhere/src/content/controller/ui/floatingPanel/pages/debug/DebugPage.tsx b/packages/danmaku-anywhere/src/content/controller/ui/floatingPanel/pages/debug/DebugPage.tsx index 71e948bb..bdb0acff 100644 --- a/packages/danmaku-anywhere/src/content/controller/ui/floatingPanel/pages/debug/DebugPage.tsx +++ b/packages/danmaku-anywhere/src/content/controller/ui/floatingPanel/pages/debug/DebugPage.tsx @@ -23,7 +23,7 @@ export const DebugPage = () => { const state = useStore() const { data: options } = useExtensionOptions() - const handleCopyState = () => { + const handleCopyState = async () => { // biome-ignore lint/suspicious/noExplicitAny: debug page serialization const snapshot = produce(state, (draft: any) => { delete draft.danmaku.comments @@ -39,9 +39,13 @@ export const DebugPage = () => { ) draft.options = options }) - void navigator.clipboard.writeText(JSON.stringify(snapshot, null, 2)) - setCopied(true) - setTimeout(() => setCopied(false), 1500) + try { + await navigator.clipboard.writeText(JSON.stringify(snapshot, null, 2)) + setCopied(true) + setTimeout(() => setCopied(false), 1500) + } catch (e) { + console.error('Failed to copy debug state to clipboard', e) + } } return ( diff --git a/packages/danmaku-anywhere/src/content/player/videoSkip/VideoSkip.service.ts b/packages/danmaku-anywhere/src/content/player/videoSkip/VideoSkip.service.ts index fdf43a53..47db6149 100644 --- a/packages/danmaku-anywhere/src/content/player/videoSkip/VideoSkip.service.ts +++ b/packages/danmaku-anywhere/src/content/player/videoSkip/VideoSkip.service.ts @@ -66,6 +66,10 @@ export class VideoSkipService { debugShowSkipButton() { this.logger.debug('Showing debug skip button') + if (this.activeButton) { + this.activeButton.root.unmount() + this.activeButton = null + } const time = this.currentVideo?.currentTime ?? 120 const target = new SkipTarget({ startTime: time,