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 }) => (
+
+)
+
+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: `
+
+ `,
+})
+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 }) => (
-
-)
-
-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 }) => (
+
+)
+
+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 && (
+
+ }
+ sx={{ fontSize: 10, py: 0.25, textTransform: 'none' }}
+ onClick={(e) => {
+ e.stopPropagation()
+ debugShowSkipButton(frame.frameId)
+ }}
+ >
+ Trigger Skip Button
+
+
+ )}
+
+
+
+ )
+}
+
+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,