From d2740786fc258c2945d0b395afe6b0f158c4e388 Mon Sep 17 00:00:00 2001 From: Alan Trebugeais Date: Fri, 8 May 2026 23:38:33 +0200 Subject: [PATCH] add: drawIcons for extensions, extensions can now draw Icon from Phosphor/react fix: SliderControl is now the default for Settings Panel of the extensions fix: resetting of values on shadows/corner radius --- src/components/video-editor/SettingsPanel.tsx | 77 +++++++--------- src/components/video-editor/VideoPlayback.tsx | 20 ++++- src/lib/extensions/extensionHost.ts | 89 ++++++++++++++++++- src/lib/extensions/types.ts | 20 +++++ 4 files changed, 155 insertions(+), 51 deletions(-) diff --git a/src/components/video-editor/SettingsPanel.tsx b/src/components/video-editor/SettingsPanel.tsx index 71c9ce53..cd5106da 100644 --- a/src/components/video-editor/SettingsPanel.tsx +++ b/src/components/video-editor/SettingsPanel.tsx @@ -263,41 +263,21 @@ function ExtensionSettingsSection({ if (field.type === "slider") { return ( -
- - {field.label} - -
- { - extensionHost.setExtensionSetting( - extensionId, - field.id, - parseFloat(e.target.value), - ); - forceUpdate((n) => n + 1); - }} - className="w-20 h-1 accent-[#2563EB]" - /> - - {(typeof value === "number" - ? value - : (field.defaultValue as number) - ).toFixed(1)} - -
+
+ { + extensionHost.setExtensionSetting(extensionId, field.id, v); + forceUpdate((n) => n + 1); + }} + formatValue={(v) => v.toFixed(1)} + parseInput={(text) => parseFloat(text)} + />
); } @@ -1215,9 +1195,12 @@ export function SettingsPanel({ if (GRADIENTS.includes(selected)) { setGradient(selected); } + }, [selected]); - if (selected.startsWith("data:image") && !customImages.includes(selected)) { - setCustomImages((prev) => [selected, ...prev]); + useEffect(() => { + if (selected.startsWith("data:image")) { + setCustomImages((prev) => (prev.includes(selected) ? prev : [selected, ...prev])); + return; } const isKnownWallpaper = @@ -1225,16 +1208,11 @@ export function SettingsPanel({ wallpaperPreviewPaths.includes(selected) || extensionWallpaperPaths.includes(selected); - if ( - !isKnownWallpaper && - isVideoWallpaperSource(selected) && - !customImages.includes(selected) - ) { - setCustomImages((prev) => [selected, ...prev]); + if (!isKnownWallpaper && isVideoWallpaperSource(selected)) { + setCustomImages((prev) => (prev.includes(selected) ? prev : [selected, ...prev])); } }, [ builtInWallpaperPaths, - customImages, extensionWallpaperPaths, selected, wallpaperPreviewPaths, @@ -1480,6 +1458,13 @@ export function SettingsPanel({ const resetBackgroundSection = () => { onBackgroundBlurChange?.(initialEditorPreferences.backgroundBlur); + onWallpaperChange( + initialEditorPreferences.wallpaper || + builtInWallpaperPaths[0] || + extensionWallpaperPaths[0] || + BUILT_IN_WALLPAPERS[0]?.publicPath || + "", + ); }; const resetZoomSection = () => { @@ -1558,7 +1543,11 @@ export function SettingsPanel({ }; const resetFrameSection = () => { + onShadowChange?.(initialEditorPreferences.shadowIntensity); + onBorderRadiusChange?.(initialEditorPreferences.borderRadius); onAspectRatioChange?.(initialEditorPreferences.aspectRatio); + onPaddingChange?.({ ...initialEditorPreferences.padding }); + onFrameChange?.(initialEditorPreferences.frame); removeBackgroundStateRef.current = null; }; diff --git a/src/components/video-editor/VideoPlayback.tsx b/src/components/video-editor/VideoPlayback.tsx index 836af7d5..47ebf8a6 100644 --- a/src/components/video-editor/VideoPlayback.tsx +++ b/src/components/video-editor/VideoPlayback.tsx @@ -430,6 +430,14 @@ const VideoPlayback = forwardRef( const [pixiRendererBackend, setPixiRendererBackend] = useState( null, ); + const [frameUpdateCounter, setFrameUpdateCounter] = useState(0); + + useEffect(() => { + return extensionHost.onChange(() => { + setFrameUpdateCounter((c) => c + 1); + }); + }, []); + const overlayRef = useRef(null); const focusIndicatorRef = useRef(null); const webcamVideoRef = useRef(null); @@ -998,10 +1006,14 @@ const VideoPlayback = forwardRef( const frameContainer = frameContainerRef.current; if (!frameContainer) return; - // Clear existing frame sprite + // Clear existing frame sprite and its texture to free memory if (frameSpriteRef.current) { - frameContainer.removeChild(frameSpriteRef.current); - frameSpriteRef.current.destroy(); + const sprite = frameSpriteRef.current; + frameContainer.removeChild(sprite); + if (sprite.texture) { + sprite.texture.destroy(true); // destroy texture and its baseTexture + } + sprite.destroy(); frameSpriteRef.current = null; } @@ -1065,7 +1077,7 @@ const VideoPlayback = forwardRef( return () => { cancelled = true; }; - }, [frame]); + }, [frame, frameUpdateCounter]); const selectedZoom = useMemo(() => { if (!selectedZoomId) return null; diff --git a/src/lib/extensions/extensionHost.ts b/src/lib/extensions/extensionHost.ts index 1a9147ab..9e9578f4 100644 --- a/src/lib/extensions/extensionHost.ts +++ b/src/lib/extensions/extensionHost.ts @@ -6,6 +6,7 @@ */ import { createExtensionModuleUrl, resolveExtensionRelativeFileUrl } from "./fileUrls"; +import * as PhosphorIcons from "@phosphor-icons/react"; import type { ContributedCursorStyle, ContributedFrame, @@ -143,6 +144,9 @@ export class ExtensionHost { Set<(settingId: string, value: unknown) => void> >(); private listeners = new Set<() => void>(); + private fullSettingsStore: Record> | null = null; + private persistTimeout: any = null; + private iconPathCache = new Map(); // Shared playback/project state — set by the app, queried by extensions private _videoInfo: { width: number; height: number; durationMs: number; fps: number } | null = @@ -468,6 +472,7 @@ export class ExtensionHost { borderRadius: layout.borderRadius, padding: normalizedPadding, }; + this.notifyListeners(); } setZoomState( @@ -478,6 +483,7 @@ export class ExtensionHost { setShadowConfig(config: { enabled: boolean; intensity: number }): void { this._shadowConfig = config; + this.notifyListeners(); } setCursorTelemetry( @@ -572,12 +578,20 @@ export class ExtensionHost { } } + private getFullSettingsStore(): Record> { + if (this.fullSettingsStore) { + return this.fullSettingsStore; + } + this.fullSettingsStore = this.readPersistedSettingsStore(); + return this.fullSettingsStore; + } + private ensureExtensionSettingsLoaded(extensionId: string): void { if (this.extensionSettings.has(extensionId)) { return; } - const store = this.readPersistedSettingsStore(); + const store = this.getFullSettingsStore(); const persisted = store[extensionId]; const normalized = persisted && typeof persisted === "object" && !Array.isArray(persisted) @@ -588,7 +602,7 @@ export class ExtensionHost { } private persistExtensionSettings(extensionId: string): void { - const store = this.readPersistedSettingsStore(); + const store = this.getFullSettingsStore(); const settings = this.extensionSettings.get(extensionId) ?? {}; if (Object.keys(settings).length === 0) { @@ -597,7 +611,14 @@ export class ExtensionHost { store[extensionId] = { ...settings }; } - this.writePersistedSettingsStore(store); + // Debounce the actual write to localStorage to avoid blocking the UI thread during rapid changes + if (this.persistTimeout) { + clearTimeout(this.persistTimeout); + } + this.persistTimeout = setTimeout(() => { + this.writePersistedSettingsStore(store); + this.persistTimeout = null; + }, 500); } /** @@ -957,6 +978,68 @@ export class ExtensionHost { }; }, + drawIcon( + ctx: CanvasRenderingContext2D, + name: string, + x: number, + y: number, + size: number, + color: string, + weight: "thin" | "light" | "regular" | "bold" | "fill" = "regular", + ): void { + const cacheKey = `${name}:${weight}`; + let path = host.iconPathCache.get(cacheKey); + + if (!path) { + // 1. Get the Icon component from the project's library + const Icon = (PhosphorIcons as any)[name]; + if (!Icon) return; + + try { + // 2. Extract path data by dry-running the component + const Icon = (PhosphorIcons as any)[name]; + if (!Icon) { + console.warn(`[extensions] Icon ${name} not found in Phosphor library`); + return; + } + + const element = (Icon as any).render?.({ weight }, null); + const weights = element?.props?.weights; + const definition = weights?.get(weight); + const children = definition?.props?.children; + + // Handle both single path and array of paths + let pathElement = children; + if (Array.isArray(children)) { + pathElement = children.find((c: any) => c?.type === 'path' || c?.props?.d); + } + + const pathData = pathElement?.props?.d; + + if (pathData) { + path = new Path2D(pathData); + host.iconPathCache.set(cacheKey, path); + } else { + console.warn(`[extensions] No path data found for ${name}:${weight}`, { element, children }); + } + } catch (err) { + console.error(`[extensions] Failed to extract path for icon ${name}:`, err); + return; + } + } + + if (path) { + ctx.save(); + ctx.translate(x, y); + const scale = size / 256; // Phosphor icons use a 256x256 grid + ctx.scale(scale, scale); + ctx.translate(-128, -128); // Center the icon + ctx.fillStyle = color; + ctx.fill(path); + ctx.restore(); + } + }, + onSettingChange(callback: (settingId: string, value: unknown) => void): () => void { if (!host.settingChangeCallbacks.has(extensionId)) { host.settingChangeCallbacks.set(extensionId, new Set()); diff --git a/src/lib/extensions/types.ts b/src/lib/extensions/types.ts index a77e58a6..4b880d8c 100644 --- a/src/lib/extensions/types.ts +++ b/src/lib/extensions/types.ts @@ -565,6 +565,26 @@ export interface RecordlyExtensionAPI { * Useful for bulk-reading initial state on activation. */ getAllSettings(): Record; + + /** + * Draw a Phosphor icon from the project's library onto the canvas. + * @param ctx The 2D rendering context + * @param name The name of the icon (e.g. "CaretLeft", "ArrowClockwise") + * @param x X coordinate (center) + * @param y Y coordinate (center) + * @param size Icon size in pixels + * @param color Icon color (CSS color string) + * @param weight Icon weight (optional: 'thin' | 'light' | 'regular' | 'bold' | 'fill', default 'regular') + */ + drawIcon( + ctx: CanvasRenderingContext2D, + name: string, + x: number, + y: number, + size: number, + color: string, + weight?: "thin" | "light" | "regular" | "bold" | "fill", + ): void; } // ---------------------------------------------------------------------------