Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 22 additions & 2 deletions src/components/DownloadResult.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { formatBytes } from "@/lib/utils";
import { Download, RotateCcw, Share2, AlertCircle, Volume2, VolumeX } from "lucide-react";
import LottiePlayer from "./LottiePlayer";
import { NativeShareButton } from "./NativeShareButton";
import successAnim from "@/lib/lottie/success.json";
import { cn } from "@/lib/utils";

const SHARE_TWEET_TEXT =
Expand All @@ -32,6 +31,7 @@ interface Props {
export default function DownloadResult({ result, onReset, soundOnCompletion, onToggleSound }: Props) {
const defaultName = `reframe_${result.width}x${result.height}`;
const [name, setName] = useState(defaultName);
const [successAnim, setSuccessAnim] = useState<object | null>(null);

const invalidCharRegex = /[<>:"/\\|?*]/;
const isValid = !invalidCharRegex.test(name) && name.trim().length > 0;
Expand All @@ -50,6 +50,26 @@ export default function DownloadResult({ result, onReset, soundOnCompletion, onT
});
}
}, [soundOnCompletion]);

useEffect(() => {
let cancelled = false;

import("@/lib/lottie/success.json")
.then((mod) => {
if (!cancelled) {
setSuccessAnim(mod.default ?? mod);
}
})
.catch(() => {
if (!cancelled) {
setSuccessAnim(null);
}
});

return () => {
cancelled = true;
};
}, []);
const handleReset = () => {
if (window.confirm("This will clear the current video and all settings. Continue?")) {
onReset();
Expand All @@ -61,7 +81,7 @@ export default function DownloadResult({ result, onReset, soundOnCompletion, onT
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<div className="w-12 h-12 shrink-0">
<LottiePlayer animationData={successAnim} loop={false} autoplay />
{successAnim ? <LottiePlayer animationData={successAnim} loop={false} autoplay /> : null}
</div>
<div>
<p className="font-heading font-bold text-base text-[var(--text)]">Export complete</p>
Expand Down
36 changes: 29 additions & 7 deletions src/components/ExportOverlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import FocusTrap from "focus-trap-react";
import { useEffect, useRef, useCallback, useState } from "react";
import { ExportStatus } from "@/lib/types";
import LottiePlayer from "./LottiePlayer";
import spinnerAnim from "@/lib/lottie/spinner.json";
import TipCarousel from "./TipCarousel";

interface Props {
Expand All @@ -26,6 +25,7 @@ export default function ExportOverlay({ status, progress, exportStartedAt, onCan
const previousFocusRef = useRef<HTMLElement | null>(null);
const focusAnchorRef = useRef<HTMLDivElement | null>(null);
const [elapsedMs, setElapsedMs] = useState(0);
const [spinnerAnim, setSpinnerAnim] = useState<object | null>(null);

const handleKeyDown = useCallback((e: KeyboardEvent) => {
if (e.key === "Escape") {
Expand Down Expand Up @@ -74,6 +74,26 @@ export default function ExportOverlay({ status, progress, exportStartedAt, onCan
return () => window.clearInterval(timer);
}, [status, exportStartedAt]);

useEffect(() => {
let cancelled = false;

import("@/lib/lottie/spinner.json")
.then((mod) => {
if (!cancelled) {
setSpinnerAnim(mod.default ?? mod);
}
})
.catch(() => {
if (!cancelled) {
setSpinnerAnim(null);
}
});

return () => {
cancelled = true;
};
}, []);

if (!visible) return null;

const isLoading = status === "loading-engine";
Expand Down Expand Up @@ -105,12 +125,14 @@ export default function ExportOverlay({ status, progress, exportStartedAt, onCan
aria-hidden="true"
/>
<div className="mx-auto w-20 h-20">
<LottiePlayer
animationData={spinnerAnim}
loop
autoplay
aria-hidden="true"
/>
{spinnerAnim ? (
<LottiePlayer
animationData={spinnerAnim}
loop
autoplay
aria-hidden="true"
/>
) : null}
</div>
<div className="export-text">
<h2 className="font-heading font-bold text-xl tracking-tight text-[var(--text)]">
Expand Down
28 changes: 24 additions & 4 deletions src/components/FileUpload.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import { useRef, useState, useEffect, useCallback } from "react";
import { Film, FolderOpen } from "lucide-react";
import LottiePlayer from "./LottiePlayer";
import uploadAnim from "@/lib/lottie/upload.json";
import { cn, formatBytes, formatDuration } from "@/lib/utils";
import { MAX_FILE_SIZE, WARNING_FILE_SIZE } from "@/lib/types";

Expand All @@ -26,6 +25,7 @@ export default function FileUpload({
const [pageDragging, setPageDragging] = useState(false);
const [error, setError] = useState("");
const [warning, setWarning] = useState("");
const [uploadAnim, setUploadAnim] = useState<object | null>(null);
const dragCounterRef = useRef(0);

// ── Keyboard shortcut Ctrl+O ──────────────────────────
Expand All @@ -40,6 +40,26 @@ export default function FileUpload({
return () => document.removeEventListener("keydown", handler);
}, []);

useEffect(() => {
let cancelled = false;

import("@/lib/lottie/upload.json")
.then((mod) => {
if (!cancelled) {
setUploadAnim(mod.default ?? mod);
}
})
.catch(() => {
if (!cancelled) {
setUploadAnim(null);
}
});

return () => {
cancelled = true;
};
}, []);

// ── Page-level drag overlay ───────────────────────────
// Uses a counter so nested dragenter/dragleave don't flicker
useEffect(() => {
Expand Down Expand Up @@ -174,7 +194,7 @@ export default function FileUpload({
<input
ref={inputRef}
type="file"
accept="video/*"
accept="video/*,.mp4,.mov,.avi,.mkv,.webm"
className="hidden"
onChange={(e) => {
const f = e.target.files?.[0];
Expand Down Expand Up @@ -216,7 +236,7 @@ export default function FileUpload({
)}

<div className="w-20 h-20 opacity-80 group-hover:opacity-100 transition-opacity group-hover:scale-110 duration-200">
<LottiePlayer animationData={uploadAnim} loop autoplay />
{uploadAnim ? <LottiePlayer animationData={uploadAnim} loop autoplay /> : null}
</div>

<div className="text-center">
Expand Down Expand Up @@ -305,7 +325,7 @@ export default function FileUpload({
<input
ref={inputRef}
type="file"
accept="video/*"
accept="video/*,.mp4,.mov,.avi,.mkv,.webm"
className="hidden"
onChange={(e) => {
const f = e.target.files?.[0];
Expand Down
6 changes: 5 additions & 1 deletion src/components/OnboardingTour.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,11 @@ export default function OnboardingTour() {

// Initialise on mount
useEffect(() => {
if (localStorage.getItem(TOUR_KEY)) return;
try {
if (localStorage.getItem(TOUR_KEY)) return;
} catch {
return;
}
const t = setTimeout(async () => {
const rect = await measureTarget(TOUR_STEPS[0]?.targetId ?? "");
if (rect) {
Expand Down
2 changes: 1 addition & 1 deletion src/lib/ffmpeg.worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -486,7 +486,7 @@ async function runExport(request: ExportRequest): Promise<ResultPayload> {
}

let missingAudioDetected = false;
const logListener = ({ message }: { message: string }) => {
logListener = ({ message }: { message: string }) => {
const msg = message.toLowerCase();
if (
msg.includes("matches no streams") ||
Expand Down
11 changes: 5 additions & 6 deletions src/lib/text-overlay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,15 +91,14 @@ export function buildTextFilter(
// Build the drawtext filter with font support
let filter = `drawtext=text='${escapedText}':x=${pixelX}:y=${pixelY}:fontsize=${overlay.fontSize}:fontcolor=${overlay.color}:fontweight=${fontWeightParam}`;

// Add font family if specified
if (overlay.fontFamily) {
// Sanitize font name for FFmpeg
// Use font file path if available, otherwise fall back to font name
if (fontFileParam) {
filter += `:${fontFileParam}`;
} else if (overlay.fontFamily) {
// Sanitize font name for FFmpeg (fallback when no font path available)
const safeFontName = overlay.fontFamily.replace(/[^a-zA-Z0-9-]/g, "");
filter += `:fontfile='${safeFontName}'`;
}

// Add custom font file path if available
if (fontFileParam) {
filter += `:${fontFileParam}`;
}

Expand Down
1 change: 1 addition & 0 deletions src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ export function isValidRecipe(value: unknown): value is EditRecipe {
if (typeof v.brightness !== "number" || !isFinite(v.brightness)) return false;
if (typeof v.contrast !== "number" || !isFinite(v.contrast)) return false;
if (typeof v.saturation !== "number" || !isFinite(v.saturation)) return false;
if (typeof v.denoise !== "boolean") return false;
if (typeof v.soundOnCompletion !== "boolean") return false;
if (!Array.isArray(v.textOverlays)) return false;

Expand Down