mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-26 07:45:34 +00:00
add: performance fix in timeline editor and warning removal
This commit is contained in:
@@ -3,7 +3,7 @@ import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const skeletonVariants = cva(
|
||||
"relative overflow-hidden rounded-md transition-shadow",
|
||||
"relative overflow-hidden rounded-md transition-shadow flex items-center justify-center",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
@@ -11,12 +11,17 @@ const skeletonVariants = cva(
|
||||
glass: "bg-white/5 backdrop-blur-sm border border-white/10",
|
||||
dark: "bg-black/20",
|
||||
subtle: "bg-foreground/[0.03]",
|
||||
clip: "bg-primary/5 border border-primary/10 shadow-inner",
|
||||
},
|
||||
animation: {
|
||||
none: "",
|
||||
pulse: "animate-pulse",
|
||||
shimmer: "before:absolute before:inset-0 before:-translate-x-full before:animate-shimmer before:bg-gradient-to-r before:from-transparent before:via-foreground/[0.04] before:to-transparent",
|
||||
"shimmer-glass": "before:absolute before:inset-0 before:-translate-x-full before:animate-shimmer before:bg-gradient-to-r before:from-transparent before:via-white/[0.08] before:to-transparent",
|
||||
shimmer:
|
||||
"before:absolute before:inset-0 before:-translate-x-full before:animate-shimmer before:bg-gradient-to-r before:from-transparent before:via-foreground/[0.05] before:to-transparent",
|
||||
"shimmer-glass":
|
||||
"before:absolute before:inset-0 before:-translate-x-full before:animate-shimmer before:bg-gradient-to-r before:from-transparent before:via-white/[0.08] before:to-transparent",
|
||||
"shimmer-premium":
|
||||
"before:absolute before:inset-0 before:-translate-x-full before:animate-[shimmer_1.5s_infinite] before:bg-gradient-to-r before:from-transparent before:via-white/[0.1] before:to-transparent",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
@@ -28,20 +33,46 @@ const skeletonVariants = cva(
|
||||
|
||||
export interface SkeletonProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof skeletonVariants> {}
|
||||
VariantProps<typeof skeletonVariants> {
|
||||
label?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A magnificent Skeleton component designed with Apple-inspired aesthetics.
|
||||
* Supports shimmer animations, pulse effects, and glassmorphism.
|
||||
* Supports shimmer animations, pulse effects, and labels.
|
||||
*/
|
||||
const Skeleton = React.forwardRef<HTMLDivElement, SkeletonProps>(
|
||||
({ className, variant, animation, ...props }, ref) => {
|
||||
({ className, variant, animation, label, children, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(skeletonVariants({ variant, animation, className }))}
|
||||
{...props}
|
||||
/>
|
||||
>
|
||||
{(label || children) && (
|
||||
<div className="relative z-10 flex flex-col items-center gap-2 px-4 py-2">
|
||||
{label && (
|
||||
<div className="flex items-center">
|
||||
{label.split("").map((char, i) => (
|
||||
<span
|
||||
key={i}
|
||||
className={cn(
|
||||
"text-[11px] font-medium tracking-tight bg-gradient-to-r from-foreground/30 via-foreground/70 to-foreground/30 bg-clip-text text-transparent animate-text-shimmer whitespace-pre",
|
||||
)}
|
||||
style={{
|
||||
animationDelay: `${i * 0.05}s`,
|
||||
animationDuration: "2.5s",
|
||||
}}
|
||||
>
|
||||
{char}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -6356,15 +6356,17 @@ export default function VideoEditor() {
|
||||
>
|
||||
<TimelineEditor
|
||||
ref={timelineRef}
|
||||
hideToolbar
|
||||
videoDuration={timelineDuration}
|
||||
currentTime={currentTime}
|
||||
playheadTime={timelinePlayheadTime}
|
||||
onSeek={handleTimelineSeek}
|
||||
videoPath={videoPath}
|
||||
videoSourcePath={videoSourcePath}
|
||||
cursorTelemetrySourcePath={cursorTelemetrySourcePath}
|
||||
cursorTelemetry={normalizedCursorTelemetry}
|
||||
autoSuggestZoomsTrigger={autoSuggestZoomsTrigger}
|
||||
onAutoSuggestZoomsConsumed={handleAutoSuggestZoomsConsumed}
|
||||
disableSuggestedZooms={!autoApplyFreshRecordingAutoZooms}
|
||||
zoomRegions={zoomRegions}
|
||||
onZoomAdded={handleZoomAdded}
|
||||
onZoomSuggested={handleZoomSuggested}
|
||||
@@ -6391,6 +6393,10 @@ export default function VideoEditor() {
|
||||
selectedAnnotationId={selectedAnnotationId}
|
||||
onSelectAnnotation={handleSelectAnnotation}
|
||||
aspectRatio={aspectRatio}
|
||||
onAspectRatioChange={setAspectRatio}
|
||||
onOpenCropEditor={handleOpenCropEditor}
|
||||
isCropped={isCropped}
|
||||
hideToolbar={timelineCollapsed}
|
||||
showSourceAudioTrack={clipRegions.some((c) => c.showSourceAudio)}
|
||||
sourceAudioTrackSettings={audio.activeSourceAudioTrackSettings}
|
||||
getSourceAudioTrackSettingsForClip={
|
||||
|
||||
@@ -12,6 +12,7 @@ import type { Span } from "dnd-timeline";
|
||||
import { useItem } from "dnd-timeline";
|
||||
import { useMemo } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import AudioWaveform from "./components/waveform/AudioWaveform";
|
||||
import type { AudioPeaksData } from "./core/timelineTypes";
|
||||
import glassStyles from "./ItemGlass.module.css";
|
||||
@@ -34,6 +35,8 @@ interface ItemProps {
|
||||
waveformNormalize?: boolean;
|
||||
muted?: boolean;
|
||||
variant?: "zoom" | "trim" | "clip" | "annotation" | "speed" | "audio";
|
||||
isLoading?: boolean;
|
||||
loadingLabel?: string;
|
||||
}
|
||||
|
||||
// Map zoom depth to multiplier labels
|
||||
@@ -73,15 +76,46 @@ export default function Item({
|
||||
waveformNormalize = false,
|
||||
muted = false,
|
||||
variant = "zoom",
|
||||
isLoading = false,
|
||||
loadingLabel,
|
||||
children,
|
||||
}: ItemProps) {
|
||||
const { setNodeRef, attributes, listeners, itemStyle, itemContentStyle } = useItem({
|
||||
id,
|
||||
span,
|
||||
disabled,
|
||||
disabled: disabled || isLoading,
|
||||
data: { rowId },
|
||||
});
|
||||
|
||||
const timeLabel = useMemo(
|
||||
() => `${formatMs(span.start)} – ${formatMs(span.end)}`,
|
||||
[span.start, span.end],
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
style={{
|
||||
...itemStyle,
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
}}
|
||||
{...listeners}
|
||||
{...attributes}
|
||||
>
|
||||
<Skeleton
|
||||
variant="clip"
|
||||
animation="shimmer-premium"
|
||||
label={loadingLabel || "Loading..."}
|
||||
className="w-full"
|
||||
style={{ height: "85%", minHeight: 22 }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const isZoom = variant === "zoom";
|
||||
const isTrim = variant === "trim";
|
||||
const isClip = variant === "clip";
|
||||
@@ -101,11 +135,6 @@ export default function Item({
|
||||
? glassStyles.glassDarkGreen
|
||||
: glassStyles.glassYellow;
|
||||
|
||||
const timeLabel = useMemo(
|
||||
() => `${formatMs(span.start)} – ${formatMs(span.end)}`,
|
||||
[span.start, span.end],
|
||||
);
|
||||
|
||||
const MIN_ITEM_PX = 6;
|
||||
const handleSelect = () => {
|
||||
onSelect?.();
|
||||
|
||||
@@ -40,7 +40,6 @@ import { calculateTimelineScale } from "./core/time";
|
||||
import { useTimelineEditorRuntime } from "./hooks/useTimelineEditorRuntime";
|
||||
import { useTimelineRange } from "./hooks/useTimelineRange";
|
||||
import TimelineCanvas from "./components/viewport/TimelineCanvas";
|
||||
import TimelineToolbar from "./components/toolbar/TimelineToolbar";
|
||||
|
||||
export interface TimelineEditorProps {
|
||||
videoDuration: number;
|
||||
@@ -85,6 +84,8 @@ export interface TimelineEditorProps {
|
||||
onOpenCropEditor?: () => void;
|
||||
isCropped?: boolean;
|
||||
videoPath?: string | null;
|
||||
videoSourcePath?: string | null;
|
||||
cursorTelemetrySourcePath?: string | null;
|
||||
hideToolbar?: boolean;
|
||||
showSourceAudioTrack?: boolean;
|
||||
onSourceAudioAvailabilityChange?: (available: boolean) => void;
|
||||
@@ -175,6 +176,8 @@ const TimelineEditor = forwardRef<TimelineEditorHandle, TimelineEditorProps>(
|
||||
onOpenCropEditor,
|
||||
isCropped = false,
|
||||
videoPath,
|
||||
videoSourcePath,
|
||||
cursorTelemetrySourcePath,
|
||||
hideToolbar = false,
|
||||
showSourceAudioTrack = false,
|
||||
onSourceAudioAvailabilityChange,
|
||||
@@ -272,7 +275,7 @@ const TimelineEditor = forwardRef<TimelineEditorHandle, TimelineEditorProps>(
|
||||
return { previewSpans, hiddenZoomIds };
|
||||
}, [clipRegions, liveSpanPreviewById, zoomRegions]);
|
||||
const { shortcuts: keyShortcuts, isMac } = useShortcuts();
|
||||
const sourceAudioPeaks = useTimelineAudioPeaks(videoPath, {
|
||||
const { peaks: sourceAudioPeaks, loading: sourceAudioLoading } = useTimelineAudioPeaks(videoPath, {
|
||||
enableSourceSidecarFallback: true,
|
||||
});
|
||||
const localSourcePath = useMemo(() => {
|
||||
@@ -290,8 +293,8 @@ const TimelineEditor = forwardRef<TimelineEditorHandle, TimelineEditorProps>(
|
||||
() => (localSourcePath ? buildSourceSidecarPath(localSourcePath, "system") : null),
|
||||
[localSourcePath],
|
||||
);
|
||||
const micSidecarPeaks = useTimelineAudioPeaks(micSidecarPath);
|
||||
const systemSidecarPeaks = useTimelineAudioPeaks(systemSidecarPath);
|
||||
const { peaks: micSidecarPeaks, loading: micSidecarLoading } = useTimelineAudioPeaks(micSidecarPath);
|
||||
const { peaks: systemSidecarPeaks, loading: systemSidecarLoading } = useTimelineAudioPeaks(systemSidecarPath);
|
||||
const sourceAudioTracks = useMemo<SourceAudioTrackWithPeaks[]>(() => {
|
||||
if (systemSidecarPeaks || micSidecarPeaks) {
|
||||
const tracks: SourceAudioTrackWithPeaks[] = [];
|
||||
@@ -319,6 +322,17 @@ const TimelineEditor = forwardRef<TimelineEditorHandle, TimelineEditorProps>(
|
||||
]
|
||||
: [];
|
||||
}, [micSidecarPeaks, sourceAudioPeaks, systemSidecarPeaks, t]);
|
||||
|
||||
const isLoading = useMemo(() => {
|
||||
// If we are still actively trying to load audio peaks (main or sidecars)
|
||||
if (videoPath && (sourceAudioLoading || micSidecarLoading || systemSidecarLoading)) return true;
|
||||
|
||||
// Robust telemetry loading detection:
|
||||
// If a source path is set but telemetry hasn't arrived (or failed/retried) for it yet.
|
||||
if (videoSourcePath && cursorTelemetrySourcePath !== videoSourcePath) return true;
|
||||
|
||||
return false;
|
||||
}, [videoPath, videoSourcePath, cursorTelemetrySourcePath, sourceAudioLoading, micSidecarLoading, systemSidecarLoading]);
|
||||
useEffect(() => {
|
||||
onSourceAudioTracksMetaChange?.(sourceAudioTracks.map((t) => ({ id: t.id, label: t.label })));
|
||||
}, [onSourceAudioTracksMetaChange, sourceAudioTracks]);
|
||||
@@ -441,12 +455,6 @@ const TimelineEditor = forwardRef<TimelineEditorHandle, TimelineEditorProps>(
|
||||
keyShortcuts,
|
||||
isTimelineFocusedRef,
|
||||
});
|
||||
const handleToolbarAddAnnotation = useCallback(() => {
|
||||
handleAddAnnotation();
|
||||
}, [handleAddAnnotation]);
|
||||
const handleToolbarAddAudio = useCallback(() => {
|
||||
void handleAddAudio();
|
||||
}, [handleAddAudio]);
|
||||
|
||||
if (!videoDuration || videoDuration === 0) {
|
||||
return (
|
||||
@@ -466,32 +474,6 @@ const TimelineEditor = forwardRef<TimelineEditorHandle, TimelineEditorProps>(
|
||||
|
||||
return (
|
||||
<div className="flex-1 min-h-0 flex flex-col bg-editor-bg overflow-hidden">
|
||||
{hideToolbar ? null : (
|
||||
<TimelineToolbar
|
||||
aspectRatio={aspectRatio}
|
||||
isCropped={isCropped}
|
||||
scrollLabels={scrollLabels}
|
||||
customAspectWidth={customAspectWidth}
|
||||
customAspectHeight={customAspectHeight}
|
||||
onCustomAspectWidthChange={setCustomAspectWidth}
|
||||
onCustomAspectHeightChange={setCustomAspectHeight}
|
||||
onCustomAspectRatioKeyDown={handleCustomAspectRatioKeyDown}
|
||||
onApplyCustomAspectRatio={applyCustomAspectRatio}
|
||||
onAspectRatioChange={onAspectRatioChange}
|
||||
onOpenCropEditor={onOpenCropEditor}
|
||||
onAddZoom={handleAddZoom}
|
||||
onSuggestZooms={handleSuggestZooms}
|
||||
onAddAnnotation={handleToolbarAddAnnotation}
|
||||
onAddAudio={handleToolbarAddAudio}
|
||||
onSplitClip={handleSplitClip}
|
||||
cropLabel={t("sections.crop", "Crop")}
|
||||
addZoomLabel={tTimeline("zoom.addZoom", "Add Zoom (Z)")}
|
||||
suggestZoomsLabel={tTimeline("zoom.suggestZooms", "Suggest Zooms from Cursor")}
|
||||
addAnnotationLabel={tTimeline("annotation.addAnnotation", "Add Annotation (A)")}
|
||||
addAudioLabel={tTimeline("audio.label", "Audio")}
|
||||
splitClipLabel={tEditor("toolbar.splitClip", "Split Clip (C)")}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
ref={timelineContainerRef}
|
||||
className="flex-1 min-h-0 overflow-auto bg-editor-bg relative"
|
||||
@@ -573,6 +555,7 @@ const TimelineEditor = forwardRef<TimelineEditorHandle, TimelineEditorProps>(
|
||||
showSourceAudioTrack={showSourceAudioTrack}
|
||||
liveSpanPreviewById={liveZoomPreview.previewSpans}
|
||||
liveHiddenItemIds={Array.from(liveZoomPreview.hiddenZoomIds)}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</TimelineWrapper>
|
||||
</div>
|
||||
|
||||
@@ -9,6 +9,7 @@ interface PlaybackCursorProps {
|
||||
onSeek?: (time: number) => void;
|
||||
timelineRef: RefObject<HTMLDivElement>;
|
||||
keyframes?: { id: string; time: number }[];
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
export default function PlaybackCursor({
|
||||
@@ -17,6 +18,7 @@ export default function PlaybackCursor({
|
||||
onSeek,
|
||||
timelineRef,
|
||||
keyframes = [],
|
||||
isLoading = false,
|
||||
}: PlaybackCursorProps) {
|
||||
const { sidebarWidth, direction, range, valueToPixels, pixelsToValue } = useTimelineContext();
|
||||
const sideProperty = direction === "rtl" ? "right" : "left";
|
||||
@@ -100,11 +102,27 @@ export default function PlaybackCursor({
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"absolute -top-6 left-1/2 -translate-x-1/2 px-1.5 py-0.5 rounded bg-black/80 text-[10px] text-white/90 font-medium tabular-nums whitespace-nowrap border border-foreground/10 shadow-lg pointer-events-none",
|
||||
isDragging ? "opacity-100" : "opacity-0",
|
||||
"absolute -top-6 left-1/2 -translate-x-1/2 px-1.5 py-0.5 rounded bg-black/80 text-[10px] text-white/90 font-medium tabular-nums whitespace-nowrap border border-foreground/10 shadow-lg pointer-events-none transition-opacity",
|
||||
(isDragging || isLoading) ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
>
|
||||
<span className="leading-5">{formatPlayheadTime(clampedTime)}</span>
|
||||
<div className="flex items-center">
|
||||
{formatPlayheadTime(clampedTime).split("").map((char, i) => (
|
||||
<span
|
||||
key={i}
|
||||
className={cn(
|
||||
"leading-5 whitespace-pre",
|
||||
isLoading && "bg-gradient-to-r from-white/40 via-white to-white/40 bg-clip-text text-transparent animate-text-shimmer"
|
||||
)}
|
||||
style={isLoading ? {
|
||||
animationDelay: `${i * 0.05}s`,
|
||||
animationDuration: "2.5s",
|
||||
} : undefined}
|
||||
>
|
||||
{char}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -68,6 +68,7 @@ interface TimelineCanvasProps {
|
||||
showSourceAudioTrack?: boolean;
|
||||
liveSpanPreviewById?: Record<string, { start: number; end: number }>;
|
||||
liveHiddenItemIds?: string[];
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
interface TimelineHoverParams {
|
||||
@@ -244,6 +245,7 @@ interface TimelineCanvasRowsProps {
|
||||
onZoomRowMouseMove: MouseEventHandler<HTMLDivElement>;
|
||||
onZoomRowMouseLeave: MouseEventHandler<HTMLDivElement>;
|
||||
onZoomRowClick: MouseEventHandler<HTMLDivElement>;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
interface AudioItemWithWaveformProps {
|
||||
@@ -261,7 +263,7 @@ function AudioItemWithWaveform({
|
||||
isSelected,
|
||||
onSelectAudio,
|
||||
}: AudioItemWithWaveformProps) {
|
||||
const peaks = useTimelineAudioPeaks(item.audioPath ?? null);
|
||||
const { peaks } = useTimelineAudioPeaks(item.audioPath ?? null);
|
||||
const normalizedWaveformSpan = useMemo(() => {
|
||||
const duration = Math.max(0, waveformSpan.end - waveformSpan.start);
|
||||
return { start: 0, end: duration };
|
||||
@@ -310,6 +312,7 @@ const TimelineCanvasRows = memo(function TimelineCanvasRows({
|
||||
onZoomRowMouseMove,
|
||||
onZoomRowMouseLeave,
|
||||
onZoomRowClick,
|
||||
isLoading = false,
|
||||
}: TimelineCanvasRowsProps) {
|
||||
const hiddenIds = useMemo(() => new Set(liveHiddenItemIds ?? []), [liveHiddenItemIds]);
|
||||
const { clipItems, zoomItems, annotationRows, audioRows } = useMemo(() => {
|
||||
@@ -376,6 +379,8 @@ const TimelineCanvasRows = memo(function TimelineCanvasRows({
|
||||
isSelected={selectAllBlocksActive || item.id === selectedClipId}
|
||||
onSelectId={onSelectClip}
|
||||
variant="clip"
|
||||
isLoading={isLoading}
|
||||
loadingLabel="Analyzing..."
|
||||
>
|
||||
{item.label}
|
||||
</Item>
|
||||
@@ -522,6 +527,7 @@ export default function TimelineCanvas({
|
||||
showSourceAudioTrack = false,
|
||||
liveSpanPreviewById,
|
||||
liveHiddenItemIds,
|
||||
isLoading = false,
|
||||
}: TimelineCanvasProps) {
|
||||
const { setTimelineRef, style, sidebarWidth, direction, range, valueToPixels, pixelsToValue } =
|
||||
useTimelineContext();
|
||||
@@ -726,6 +732,7 @@ export default function TimelineCanvas({
|
||||
onSeek={onSeek}
|
||||
timelineRef={localTimelineRef}
|
||||
keyframes={keyframes}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
{canShowGhostPlayhead && (
|
||||
<div
|
||||
@@ -765,6 +772,7 @@ export default function TimelineCanvas({
|
||||
onZoomRowMouseMove={handleZoomRowMouseMove}
|
||||
onZoomRowMouseLeave={handleZoomRowMouseLeave}
|
||||
onZoomRowClick={handleZoomRowClick}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -40,20 +40,30 @@ interface TimelineAudioPeaksOptions {
|
||||
peakCount?: number;
|
||||
}
|
||||
|
||||
export interface TimelineAudioPeaksResult {
|
||||
peaks: AudioPeaksData | null;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
export function useTimelineAudioPeaks(
|
||||
mediaResource: string | null | undefined,
|
||||
options: TimelineAudioPeaksOptions = {},
|
||||
): AudioPeaksData | null {
|
||||
const [data, setData] = useState<AudioPeaksData | null>(null);
|
||||
): TimelineAudioPeaksResult {
|
||||
const [peaks, setPeaks] = useState<AudioPeaksData | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const sourceRef = useRef(mediaResource);
|
||||
const enableSourceSidecarFallback = options.enableSourceSidecarFallback ?? false;
|
||||
const peakCount = options.peakCount ?? WAVEFORM_DEFAULT_PEAK_COUNT;
|
||||
|
||||
useEffect(() => {
|
||||
sourceRef.current = mediaResource;
|
||||
setData(null);
|
||||
if (!mediaResource) return;
|
||||
setPeaks(null);
|
||||
if (!mediaResource) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
let cancelled = false;
|
||||
|
||||
const run = async () => {
|
||||
@@ -64,29 +74,50 @@ export function useTimelineAudioPeaks(
|
||||
|
||||
try {
|
||||
const result = await tryGenerate(mediaResource);
|
||||
if (!cancelled && sourceRef.current === mediaResource) setData(result);
|
||||
if (!cancelled && sourceRef.current === mediaResource) {
|
||||
setPeaks(result);
|
||||
setLoading(false);
|
||||
}
|
||||
return;
|
||||
} catch {
|
||||
// fallthrough
|
||||
}
|
||||
|
||||
if (!enableSourceSidecarFallback) return;
|
||||
if (!enableSourceSidecarFallback) {
|
||||
if (!cancelled && sourceRef.current === mediaResource) {
|
||||
setLoading(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const localPathFromServer = extractLocalPathFromMediaServerUrl(mediaResource);
|
||||
const localSourcePath =
|
||||
localPathFromServer ||
|
||||
(/^file:\/\//i.test(mediaResource) ? fromFileUrl(mediaResource) : mediaResource);
|
||||
if (!localSourcePath) return;
|
||||
if (!localSourcePath) {
|
||||
if (!cancelled && sourceRef.current === mediaResource) {
|
||||
setLoading(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
for (const candidate of buildSidecarAudioCandidates(localSourcePath)) {
|
||||
const candidates = buildSidecarAudioCandidates(localSourcePath);
|
||||
for (const candidate of candidates) {
|
||||
try {
|
||||
const result = await tryGenerate(candidate);
|
||||
if (!cancelled && sourceRef.current === mediaResource) setData(result);
|
||||
if (!cancelled && sourceRef.current === mediaResource) {
|
||||
setPeaks(result);
|
||||
setLoading(false);
|
||||
}
|
||||
return;
|
||||
} catch {
|
||||
// try next
|
||||
}
|
||||
}
|
||||
|
||||
if (!cancelled && sourceRef.current === mediaResource) {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
void run();
|
||||
@@ -96,5 +127,5 @@ export function useTimelineAudioPeaks(
|
||||
};
|
||||
}, [mediaResource, enableSourceSidecarFallback, peakCount]);
|
||||
|
||||
return data;
|
||||
return { peaks, loading };
|
||||
}
|
||||
|
||||
@@ -18,11 +18,22 @@ module.exports = {
|
||||
transform: "translateX(100%)",
|
||||
},
|
||||
},
|
||||
"text-shimmer": {
|
||||
"0%, 100%": {
|
||||
"background-size": "200% 200%",
|
||||
"background-position": "left center",
|
||||
},
|
||||
"50%": {
|
||||
"background-size": "200% 200%",
|
||||
"background-position": "right center",
|
||||
},
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
"accordion-down": "accordion-down 0.2s ease-out",
|
||||
"accordion-up": "accordion-up 0.2s ease-out",
|
||||
shimmer: "shimmer 2s infinite",
|
||||
"text-shimmer": "text-shimmer 2.5s ease-out infinite",
|
||||
},
|
||||
borderRadius: {
|
||||
lg: "var(--radius)",
|
||||
|
||||
Reference in New Issue
Block a user