mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-27 08:15:42 +00:00
feat(editor): add video wallpaper background support
Adds video file detection (mp4/webm/mov/etc) to wallpaper system, renders video backgrounds with looping playback in editor preview, and adds Video tab to background picker in settings panel.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { Palette, Trash2, Upload, X } from "lucide-react";
|
||||
import { Film, Palette, Trash2, Upload, X } from "lucide-react";
|
||||
import { AnimatePresence, LayoutGroup, motion } from "motion/react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
@@ -15,7 +15,7 @@ import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
||||
import { getAssetPath, getRenderableAssetUrl } from "@/lib/assetPath";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { BuiltInWallpaper } from "@/lib/wallpapers";
|
||||
import { BUILT_IN_WALLPAPERS, getAvailableWallpapers } from "@/lib/wallpapers";
|
||||
import { BUILT_IN_WALLPAPERS, getAvailableWallpapers, isVideoWallpaperSource } from "@/lib/wallpapers";
|
||||
import { type AspectRatio } from "@/utils/aspectRatioUtils";
|
||||
import minimalCursorUrl from "../../../Minimal Cursor.svg";
|
||||
import amongusCursorUrl from "../../assets/cursors/amongus/default.png";
|
||||
@@ -105,7 +105,7 @@ const CAPTION_ANIMATION_OPTIONS: Array<{ value: AutoCaptionAnimation; label: str
|
||||
{ value: "pop", label: "Pop" },
|
||||
];
|
||||
|
||||
type BackgroundTab = "image" | "color" | "gradient";
|
||||
type BackgroundTab = "image" | "video" | "color" | "gradient";
|
||||
export type EditorEffectSection =
|
||||
| "scene"
|
||||
| "cursor"
|
||||
@@ -128,6 +128,10 @@ function getBackgroundTabForWallpaper(value: string): BackgroundTab {
|
||||
return "color";
|
||||
}
|
||||
|
||||
if (isVideoWallpaperSource(value)) {
|
||||
return "video";
|
||||
}
|
||||
|
||||
return "image";
|
||||
}
|
||||
|
||||
@@ -713,7 +717,18 @@ export function SettingsPanel({
|
||||
if (selected.startsWith("data:image") && !customImages.includes(selected)) {
|
||||
setCustomImages((prev) => [selected, ...prev]);
|
||||
}
|
||||
}, [customImages, selected]);
|
||||
|
||||
const isBuiltInWallpaper =
|
||||
builtInWallpaperPaths.includes(selected) || wallpaperPreviewPaths.includes(selected);
|
||||
|
||||
if (
|
||||
!isBuiltInWallpaper &&
|
||||
isVideoWallpaperSource(selected) &&
|
||||
!customImages.includes(selected)
|
||||
) {
|
||||
setCustomImages((prev) => [selected, ...prev]);
|
||||
}
|
||||
}, [builtInWallpaperPaths, customImages, selected, wallpaperPreviewPaths]);
|
||||
|
||||
useEffect(() => {
|
||||
saveEditorPreferences({ customWallpapers: customImages });
|
||||
@@ -767,7 +782,7 @@ export function SettingsPanel({
|
||||
);
|
||||
|
||||
const renderWallpaperImageTile = (
|
||||
imageUrl: string,
|
||||
wallpaperUrl: string,
|
||||
isSelected: boolean,
|
||||
props?: {
|
||||
key?: string;
|
||||
@@ -786,16 +801,25 @@ export function SettingsPanel({
|
||||
role="button"
|
||||
>
|
||||
<div className="absolute inset-[1px] overflow-hidden rounded-[8px] bg-[#0d0e11]">
|
||||
<img
|
||||
src={imageUrl}
|
||||
alt={
|
||||
props?.title ??
|
||||
props?.ariaLabel ??
|
||||
tSettings("background.wallpaperPreview", "Wallpaper preview")
|
||||
}
|
||||
className="h-full w-full select-none object-cover [transform:translateZ(0)]"
|
||||
draggable={false}
|
||||
/>
|
||||
{isVideoWallpaperSource(wallpaperUrl) ? (
|
||||
<div className="flex h-full w-full flex-col items-center justify-center gap-1 bg-[radial-gradient(circle_at_top,#1f3b68,transparent_58%),linear-gradient(135deg,#0f172a,#111827_48%,#1d4ed8)] px-1 text-center text-white">
|
||||
<Film className="h-4 w-4 opacity-90" />
|
||||
<span className="line-clamp-2 text-[8px] font-medium leading-tight text-white/80">
|
||||
{props?.title ?? props?.ariaLabel ?? tSettings("background.video", "Video background")}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<img
|
||||
src={wallpaperUrl}
|
||||
alt={
|
||||
props?.title ??
|
||||
props?.ariaLabel ??
|
||||
tSettings("background.wallpaperPreview", "Wallpaper preview")
|
||||
}
|
||||
className="h-full w-full select-none object-cover [transform:translateZ(0)]"
|
||||
draggable={false}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{props?.children}
|
||||
</div>
|
||||
@@ -1000,10 +1024,11 @@ export function SettingsPanel({
|
||||
|
||||
<div className="w-full">
|
||||
<LayoutGroup id="background-picker-switcher">
|
||||
<div className="grid h-8 w-full grid-cols-3 rounded-xl border border-white/10 bg-white/[0.04] p-1">
|
||||
<div className="grid h-8 w-full grid-cols-4 rounded-xl border border-white/10 bg-white/[0.04] p-1">
|
||||
{(
|
||||
[
|
||||
{ value: "image", label: tSettings("background.image") },
|
||||
{ value: "video", label: tSettings("background.video", "Video") },
|
||||
{ value: "color", label: tSettings("background.color") },
|
||||
{ value: "gradient", label: tSettings("background.gradient") },
|
||||
] as const
|
||||
@@ -1069,6 +1094,12 @@ export function SettingsPanel({
|
||||
const isSelected = getWallpaperTileState(imageUrl);
|
||||
return renderWallpaperImageTile(imageUrl, isSelected, {
|
||||
key: `custom-${idx}`,
|
||||
ariaLabel: isVideoWallpaperSource(imageUrl)
|
||||
? imageUrl.split(/[\\/]/).pop() ?? tSettings("background.video", "Video background")
|
||||
: undefined,
|
||||
title: isVideoWallpaperSource(imageUrl)
|
||||
? imageUrl.split(/[\\/]/).pop()
|
||||
: undefined,
|
||||
onClick: () => onWallpaperChange(imageUrl),
|
||||
children: (
|
||||
<button
|
||||
@@ -1098,6 +1129,10 @@ export function SettingsPanel({
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
) : backgroundTab === "video" ? (
|
||||
<div className="mt-0 flex min-h-24 items-center justify-center rounded-xl border border-dashed border-white/10 bg-white/[0.03] text-[11px] text-slate-400">
|
||||
Video backgrounds coming next.
|
||||
</div>
|
||||
) : backgroundTab === "color" ? (
|
||||
<div className="mt-0 space-y-2">
|
||||
<input
|
||||
|
||||
@@ -13,7 +13,9 @@ import { clampMediaTimeToDuration } from "@/lib/mediaTiming";
|
||||
import {
|
||||
DEFAULT_WALLPAPER_PATH,
|
||||
DEFAULT_WALLPAPER_RELATIVE_PATH,
|
||||
isVideoWallpaperSource,
|
||||
} from "@/lib/wallpapers";
|
||||
import { resolveMediaElementSource } from "@/lib/exporter/localMediaSource";
|
||||
import {
|
||||
Application,
|
||||
Container,
|
||||
@@ -1473,14 +1475,21 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
|
||||
const [resolvedWallpaper, setResolvedWallpaper] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [resolvedWallpaperKind, setResolvedWallpaperKind] = useState<
|
||||
"image" | "video" | "style"
|
||||
>("image");
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
let revokeResolvedWallpaper = () => {};
|
||||
(async () => {
|
||||
try {
|
||||
if (!wallpaper) {
|
||||
const def = await getAssetPath(DEFAULT_WALLPAPER_RELATIVE_PATH);
|
||||
if (mounted) setResolvedWallpaper(def);
|
||||
if (mounted) {
|
||||
setResolvedWallpaper(def);
|
||||
setResolvedWallpaperKind("image");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1489,13 +1498,29 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
|
||||
wallpaper.startsWith("linear-gradient") ||
|
||||
wallpaper.startsWith("radial-gradient")
|
||||
) {
|
||||
if (mounted) setResolvedWallpaper(wallpaper);
|
||||
if (mounted) {
|
||||
setResolvedWallpaper(wallpaper);
|
||||
setResolvedWallpaperKind("style");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (isVideoWallpaperSource(wallpaper)) {
|
||||
const resolved = await resolveMediaElementSource(wallpaper);
|
||||
revokeResolvedWallpaper = resolved.revoke;
|
||||
if (mounted) {
|
||||
setResolvedWallpaper(resolved.src);
|
||||
setResolvedWallpaperKind("video");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// If it's a data URL (custom uploaded image), use as-is
|
||||
if (wallpaper.startsWith("data:")) {
|
||||
if (mounted) setResolvedWallpaper(wallpaper);
|
||||
if (mounted) {
|
||||
setResolvedWallpaper(wallpaper);
|
||||
setResolvedWallpaperKind("image");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1505,20 +1530,31 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
|
||||
wallpaper.startsWith("/")
|
||||
) {
|
||||
const renderable = await getRenderableAssetUrl(wallpaper);
|
||||
if (mounted) setResolvedWallpaper(renderable);
|
||||
if (mounted) {
|
||||
setResolvedWallpaper(renderable);
|
||||
setResolvedWallpaperKind("image");
|
||||
}
|
||||
return;
|
||||
}
|
||||
const p = await getRenderableAssetUrl(
|
||||
await getAssetPath(wallpaper.replace(/^\//, "")),
|
||||
);
|
||||
if (mounted) setResolvedWallpaper(p);
|
||||
if (mounted) {
|
||||
setResolvedWallpaper(p);
|
||||
setResolvedWallpaperKind("image");
|
||||
}
|
||||
} catch (err) {
|
||||
if (mounted)
|
||||
if (mounted) {
|
||||
setResolvedWallpaper(wallpaper || DEFAULT_WALLPAPER_PATH);
|
||||
setResolvedWallpaperKind(
|
||||
isVideoWallpaperSource(wallpaper || "") ? "video" : "image",
|
||||
);
|
||||
}
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
mounted = false;
|
||||
revokeResolvedWallpaper();
|
||||
};
|
||||
}, [wallpaper]);
|
||||
|
||||
@@ -1531,7 +1567,7 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
|
||||
};
|
||||
}, []);
|
||||
|
||||
const isImageUrl = Boolean(
|
||||
const isImageUrl = resolvedWallpaperKind === "image" && Boolean(
|
||||
resolvedWallpaper &&
|
||||
(resolvedWallpaper.startsWith("file://") ||
|
||||
resolvedWallpaper.startsWith("http") ||
|
||||
@@ -1540,7 +1576,9 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
|
||||
);
|
||||
const backgroundStyle = isImageUrl
|
||||
? { backgroundImage: `url(${resolvedWallpaper || ""})` }
|
||||
: { background: resolvedWallpaper || "" };
|
||||
: resolvedWallpaperKind === "video"
|
||||
? {}
|
||||
: { background: resolvedWallpaper || "" };
|
||||
|
||||
const nativeAspectRatio = (() => {
|
||||
const locked = lockedVideoDimensionsRef.current;
|
||||
@@ -1569,13 +1607,28 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
|
||||
}}
|
||||
>
|
||||
{/* Background layer */}
|
||||
<div
|
||||
className="absolute inset-0 bg-cover bg-center"
|
||||
style={{
|
||||
...backgroundStyle,
|
||||
filter: backgroundBlur > 0 ? `blur(${backgroundBlur}px)` : "none",
|
||||
}}
|
||||
/>
|
||||
{resolvedWallpaperKind === "video" && resolvedWallpaper ? (
|
||||
<video
|
||||
key={resolvedWallpaper}
|
||||
className="absolute inset-0 h-full w-full object-cover"
|
||||
src={resolvedWallpaper}
|
||||
muted
|
||||
loop
|
||||
autoPlay
|
||||
playsInline
|
||||
style={{
|
||||
filter: backgroundBlur > 0 ? `blur(${backgroundBlur}px)` : "none",
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className="absolute inset-0 bg-cover bg-center"
|
||||
style={{
|
||||
...backgroundStyle,
|
||||
filter: backgroundBlur > 0 ? `blur(${backgroundBlur}px)` : "none",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="absolute inset-0"
|
||||
|
||||
@@ -6,6 +6,7 @@ export interface BuiltInWallpaper {
|
||||
}
|
||||
|
||||
const IMAGE_FILE_PATTERN = /\.(avif|gif|jpe?g|png|svg|webp)$/i;
|
||||
const VIDEO_FILE_PATTERN = /\.(avi|m4v|mkv|mov|mp4|webm)$/i;
|
||||
|
||||
export const BUILT_IN_WALLPAPERS: BuiltInWallpaper[] = [
|
||||
{ id: 'wallpaper-1', label: 'Wallpaper 1', relativePath: 'wallpapers/wallpaper1.jpg', publicPath: '/wallpapers/wallpaper1.jpg' },
|
||||
@@ -38,6 +39,15 @@ export const WALLPAPER_RELATIVE_PATHS = BUILT_IN_WALLPAPERS.map((wallpaper) => w
|
||||
export const DEFAULT_WALLPAPER_PATH = '/wallpapers/wallpaper2.jpg';
|
||||
export const DEFAULT_WALLPAPER_RELATIVE_PATH = 'wallpapers/wallpaper2.jpg';
|
||||
|
||||
export function isVideoWallpaperSource(value: string): boolean {
|
||||
if (!value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const normalizedValue = value.split("?")[0]?.toLowerCase() ?? value.toLowerCase();
|
||||
return VIDEO_FILE_PATTERN.test(normalizedValue);
|
||||
}
|
||||
|
||||
function toWallpaperId(fileName: string) {
|
||||
return fileName
|
||||
.replace(/\.[^.]+$/, "")
|
||||
|
||||
Reference in New Issue
Block a user