mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-24 23:05:49 +00:00
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
This commit is contained in:
@@ -263,41 +263,21 @@ function ExtensionSettingsSection({
|
||||
|
||||
if (field.type === "slider") {
|
||||
return (
|
||||
<div
|
||||
key={field.id}
|
||||
className="flex items-center justify-between gap-2 rounded-lg bg-foreground/[0.03] px-2.5 py-1.5"
|
||||
>
|
||||
<span className="text-[11px] text-muted-foreground flex-shrink-0">
|
||||
{field.label}
|
||||
</span>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<input
|
||||
type="range"
|
||||
min={field.min ?? 0}
|
||||
max={field.max ?? 1}
|
||||
step={field.step ?? 0.01}
|
||||
value={
|
||||
typeof value === "number"
|
||||
? value
|
||||
: (field.defaultValue as number)
|
||||
}
|
||||
onChange={(e) => {
|
||||
extensionHost.setExtensionSetting(
|
||||
extensionId,
|
||||
field.id,
|
||||
parseFloat(e.target.value),
|
||||
);
|
||||
forceUpdate((n) => n + 1);
|
||||
}}
|
||||
className="w-20 h-1 accent-[#2563EB]"
|
||||
/>
|
||||
<span className="text-[10px] text-muted-foreground/70 w-8 text-right font-mono">
|
||||
{(typeof value === "number"
|
||||
? value
|
||||
: (field.defaultValue as number)
|
||||
).toFixed(1)}
|
||||
</span>
|
||||
</div>
|
||||
<div key={field.id} className="mt-1">
|
||||
<SliderControl
|
||||
label={field.label}
|
||||
value={typeof value === "number" ? value : (field.defaultValue as number)}
|
||||
defaultValue={field.defaultValue as number}
|
||||
min={field.min ?? 0}
|
||||
max={field.max ?? 100}
|
||||
step={field.step ?? 1}
|
||||
onChange={(v) => {
|
||||
extensionHost.setExtensionSetting(extensionId, field.id, v);
|
||||
forceUpdate((n) => n + 1);
|
||||
}}
|
||||
formatValue={(v) => v.toFixed(1)}
|
||||
parseInput={(text) => parseFloat(text)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -430,6 +430,14 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
|
||||
const [pixiRendererBackend, setPixiRendererBackend] = useState<PixiPreviewBackend | null>(
|
||||
null,
|
||||
);
|
||||
const [frameUpdateCounter, setFrameUpdateCounter] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
return extensionHost.onChange(() => {
|
||||
setFrameUpdateCounter((c) => c + 1);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const overlayRef = useRef<HTMLDivElement | null>(null);
|
||||
const focusIndicatorRef = useRef<HTMLDivElement | null>(null);
|
||||
const webcamVideoRef = useRef<HTMLVideoElement | null>(null);
|
||||
@@ -998,10 +1006,14 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
|
||||
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<VideoPlaybackRef, VideoPlaybackProps>(
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [frame]);
|
||||
}, [frame, frameUpdateCounter]);
|
||||
|
||||
const selectedZoom = useMemo(() => {
|
||||
if (!selectedZoomId) return null;
|
||||
|
||||
@@ -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<string, Record<string, unknown>> | null = null;
|
||||
private persistTimeout: any = null;
|
||||
private iconPathCache = new Map<string, Path2D>();
|
||||
|
||||
// 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<string, Record<string, unknown>> {
|
||||
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());
|
||||
|
||||
@@ -565,6 +565,26 @@ export interface RecordlyExtensionAPI {
|
||||
* Useful for bulk-reading initial state on activation.
|
||||
*/
|
||||
getAllSettings(): Record<string, unknown>;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user