summaryrefslogtreecommitdiff
path: root/apps/web/src/components/editor
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2025-08-31 19:57:50 +0200
committerMaze Winther <mazewinther@gmail.com>2025-08-31 19:57:50 +0200
commit2c5d5dc23437e288d7e6936e0337583dc8dbc782 (patch)
tree187942264d1237a8f5c6c0a13bb94c7cc05971f4 /apps/web/src/components/editor
parent46a74b30ba3a3ff9b499677bb5d63ea3befea56b (diff)
downloadopencut-2c5d5dc23437e288d7e6936e0337583dc8dbc782.tar.gz
bunch of small improvements
Diffstat (limited to 'apps/web/src/components/editor')
-rw-r--r--apps/web/src/components/editor/export-button.tsx291
-rw-r--r--apps/web/src/components/editor/media-panel/views/captions.tsx2
-rw-r--r--apps/web/src/components/editor/panel-base-view.tsx5
-rw-r--r--apps/web/src/components/editor/preview-panel.tsx7
4 files changed, 177 insertions, 128 deletions
diff --git a/apps/web/src/components/editor/export-button.tsx b/apps/web/src/components/editor/export-button.tsx
index 41a5f65c..246e8b6f 100644
--- a/apps/web/src/components/editor/export-button.tsx
+++ b/apps/web/src/components/editor/export-button.tsx
@@ -16,7 +16,7 @@ import {
DEFAULT_EXPORT_OPTIONS,
} from "@/lib/export";
import { useProjectStore } from "@/stores/project-store";
-import { Download, X } from "lucide-react";
+import { Check, Copy, Download, RotateCcw, X } from "lucide-react";
import { ExportFormat, ExportQuality, ExportResult } from "@/types/export";
import { PropertyGroup } from "./properties-panel/property-item";
@@ -134,135 +134,184 @@ function ExportPopover({
return (
<PopoverContent className="w-80 mr-4 flex flex-col gap-3 bg-background">
<>
- <div className="flex items-center justify-between">
- <h3 className=" font-medium">
- {isExporting ? "Exporting project" : "Export project"}
- </h3>
- <Button variant="text" size="icon" onClick={handleClose}>
- <X className="!size-5 text-foreground/85" />
- </Button>
- </div>
+ {exportResult && !exportResult.success ? (
+ <ExportError
+ error={exportResult.error || "Unknown error occurred"}
+ onRetry={handleExport}
+ />
+ ) : (
+ <>
+ <div className="flex items-center justify-between">
+ <h3 className=" font-medium">
+ {isExporting ? "Exporting project" : "Export project"}
+ </h3>
+ <Button variant="text" size="icon" onClick={handleClose}>
+ <X className="!size-5 text-foreground/85" />
+ </Button>
+ </div>
- <div className="flex flex-col gap-4">
- {!isExporting && (
- <>
- <div className="flex flex-col gap-3">
- <PropertyGroup
- title="Format"
- titleClassName="text-sm"
- defaultExpanded={false}
- >
- <RadioGroup
- value={format}
- onValueChange={(value) => setFormat(value as ExportFormat)}
- >
- <div className="flex items-center space-x-2">
- <RadioGroupItem value="mp4" id="mp4" />
- <Label htmlFor="mp4">
- MP4 (H.264) - Better compatibility
- </Label>
- </div>
- <div className="flex items-center space-x-2">
- <RadioGroupItem value="webm" id="webm" />
- <Label htmlFor="webm">
- WebM (VP9) - Smaller file size
- </Label>
- </div>
- </RadioGroup>
- </PropertyGroup>
+ <div className="flex flex-col gap-4">
+ {!isExporting && (
+ <>
+ <div className="flex flex-col gap-3">
+ <PropertyGroup
+ title="Format"
+ titleClassName="text-sm"
+ defaultExpanded={false}
+ >
+ <RadioGroup
+ value={format}
+ onValueChange={(value) =>
+ setFormat(value as ExportFormat)
+ }
+ >
+ <div className="flex items-center space-x-2">
+ <RadioGroupItem value="mp4" id="mp4" />
+ <Label htmlFor="mp4">
+ MP4 (H.264) - Better compatibility
+ </Label>
+ </div>
+ <div className="flex items-center space-x-2">
+ <RadioGroupItem value="webm" id="webm" />
+ <Label htmlFor="webm">
+ WebM (VP9) - Smaller file size
+ </Label>
+ </div>
+ </RadioGroup>
+ </PropertyGroup>
- <PropertyGroup
- title="Quality"
- titleClassName="text-sm"
- defaultExpanded={false}
- >
- <RadioGroup
- value={quality}
- onValueChange={(value) =>
- setQuality(value as ExportQuality)
- }
- >
- <div className="flex items-center space-x-2">
- <RadioGroupItem value="low" id="low" />
- <Label htmlFor="low">Low - Smallest file size</Label>
- </div>
- <div className="flex items-center space-x-2">
- <RadioGroupItem value="medium" id="medium" />
- <Label htmlFor="medium">Medium - Balanced</Label>
- </div>
- <div className="flex items-center space-x-2">
- <RadioGroupItem value="high" id="high" />
- <Label htmlFor="high">High - Recommended</Label>
- </div>
- <div className="flex items-center space-x-2">
- <RadioGroupItem value="very_high" id="very_high" />
- <Label htmlFor="very_high">
- Very High - Largest file size
- </Label>
- </div>
- </RadioGroup>
- </PropertyGroup>
+ <PropertyGroup
+ title="Quality"
+ titleClassName="text-sm"
+ defaultExpanded={false}
+ >
+ <RadioGroup
+ value={quality}
+ onValueChange={(value) =>
+ setQuality(value as ExportQuality)
+ }
+ >
+ <div className="flex items-center space-x-2">
+ <RadioGroupItem value="low" id="low" />
+ <Label htmlFor="low">Low - Smallest file size</Label>
+ </div>
+ <div className="flex items-center space-x-2">
+ <RadioGroupItem value="medium" id="medium" />
+ <Label htmlFor="medium">Medium - Balanced</Label>
+ </div>
+ <div className="flex items-center space-x-2">
+ <RadioGroupItem value="high" id="high" />
+ <Label htmlFor="high">High - Recommended</Label>
+ </div>
+ <div className="flex items-center space-x-2">
+ <RadioGroupItem value="very_high" id="very_high" />
+ <Label htmlFor="very_high">
+ Very High - Largest file size
+ </Label>
+ </div>
+ </RadioGroup>
+ </PropertyGroup>
- <PropertyGroup
- title="Audio"
- titleClassName="text-sm"
- defaultExpanded={false}
- >
- <div className="flex items-center space-x-2">
- <Checkbox
- id="include-audio"
- checked={includeAudio}
- onCheckedChange={(checked) => setIncludeAudio(!!checked)}
- />
- <Label htmlFor="include-audio">
- Include audio in export
- </Label>
+ <PropertyGroup
+ title="Audio"
+ titleClassName="text-sm"
+ defaultExpanded={false}
+ >
+ <div className="flex items-center space-x-2">
+ <Checkbox
+ id="include-audio"
+ checked={includeAudio}
+ onCheckedChange={(checked) =>
+ setIncludeAudio(!!checked)
+ }
+ />
+ <Label htmlFor="include-audio">
+ Include audio in export
+ </Label>
+ </div>
+ </PropertyGroup>
</div>
- </PropertyGroup>
- </div>
- <Button onClick={handleExport} className="w-full gap-2">
- <Download className="w-4 h-4" />
- Export
- </Button>
- </>
- )}
-
- {isExporting && (
- <div className="space-y-4">
- <div className="flex flex-col">
- <div className="text-center flex items-center justify-between">
- <p className="text-sm text-muted-foreground mb-2">
- {Math.round(progress * 100)}%
- </p>
- <p className="text-sm text-muted-foreground mb-2">100%</p>
- </div>
- <Progress value={progress * 100} className="w-full" />
- </div>
+ <Button onClick={handleExport} className="w-full gap-2">
+ <Download className="w-4 h-4" />
+ Export
+ </Button>
+ </>
+ )}
- <Button
- variant="outline"
- className="rounded-md w-full"
- onClick={() => {}}
- >
- Cancel
- </Button>
- </div>
- )}
+ {isExporting && (
+ <div className="space-y-4">
+ <div className="flex flex-col">
+ <div className="text-center flex items-center justify-between">
+ <p className="text-sm text-muted-foreground mb-2">
+ {Math.round(progress * 100)}%
+ </p>
+ <p className="text-sm text-muted-foreground mb-2">100%</p>
+ </div>
+ <Progress value={progress * 100} className="w-full" />
+ </div>
- {exportResult && !exportResult.success && (
- <div className="text-center space-y-3">
- <div className="text-red-600 font-medium">Export failed</div>
- <p className="text-sm text-muted-foreground">
- {exportResult.error || "Unknown error occurred"}
- </p>
- <Button variant="outline" onClick={() => setExportResult(null)}>
- Try Again
- </Button>
+ <Button
+ variant="outline"
+ className="rounded-md w-full"
+ onClick={() => {}}
+ >
+ Cancel
+ </Button>
+ </div>
+ )}
</div>
- )}
- </div>
+ </>
+ )}
</>
</PopoverContent>
);
}
+
+function ExportError({
+ error,
+ onRetry,
+}: {
+ error: string;
+ onRetry: () => void;
+}) {
+ const [copied, setCopied] = useState(false);
+
+ const handleCopy = async () => {
+ await navigator.clipboard.writeText(error);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 1000);
+ };
+
+ return (
+ <div className="space-y-4">
+ <div className="flex flex-col gap-1.5">
+ <p className="text-sm font-medium text-red-400">Export failed</p>
+ <p className="text-xs text-muted-foreground">
+ {error}
+ </p>
+ </div>
+
+ <div className="flex gap-2">
+ <Button
+ variant="outline"
+ size="sm"
+ className="flex-1 text-xs h-8"
+ onClick={handleCopy}
+ >
+ {copied ? <Check className="text-green-500" /> : <Copy />}
+ Copy
+ </Button>
+ <Button
+ variant="outline"
+ size="sm"
+ className="flex-1 text-xs h-8"
+ onClick={onRetry}
+ >
+ <RotateCcw />
+ Retry
+ </Button>
+ </div>
+ </div>
+ );
+}
diff --git a/apps/web/src/components/editor/media-panel/views/captions.tsx b/apps/web/src/components/editor/media-panel/views/captions.tsx
index 59403a8d..fbe5b418 100644
--- a/apps/web/src/components/editor/media-panel/views/captions.tsx
+++ b/apps/web/src/components/editor/media-panel/views/captions.tsx
@@ -188,7 +188,7 @@ export function Captions() {
};
return (
- <BaseView ref={containerRef} className="flex flex-col justify-between">
+ <BaseView ref={containerRef} className="flex flex-col justify-between h-full">
<PropertyGroup title="Language">
<LanguageSelect
selectedCountry={selectedCountry}
diff --git a/apps/web/src/components/editor/panel-base-view.tsx b/apps/web/src/components/editor/panel-base-view.tsx
index ec01028c..bef4e62c 100644
--- a/apps/web/src/components/editor/panel-base-view.tsx
+++ b/apps/web/src/components/editor/panel-base-view.tsx
@@ -1,6 +1,7 @@
import { ScrollArea } from "@/components/ui/scroll-area";
import { Separator } from "@/components/ui/separator";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
+import { cn } from "@/lib/utils";
interface PanelBaseViewProps {
children?: React.ReactNode;
@@ -25,7 +26,7 @@ function ViewContent({
}) {
return (
<ScrollArea className="flex-1">
- <div className={`p-5 h-full ${className}`}>{children}</div>
+ <div className={cn("p-5", className)}>{children}</div>
</ScrollArea>
);
}
@@ -40,7 +41,7 @@ export function PanelBaseView({
ref,
}: PanelBaseViewProps) {
return (
- <div className={`h-full flex flex-col ${className}`} ref={ref}>
+ <div className={cn("h-full flex flex-col", className)} ref={ref}>
{!tabs || tabs.length === 0 ? (
<ViewContent className={className}>{children}</ViewContent>
) : (
diff --git a/apps/web/src/components/editor/preview-panel.tsx b/apps/web/src/components/editor/preview-panel.tsx
index b7cc9578..8b1abd81 100644
--- a/apps/web/src/components/editor/preview-panel.tsx
+++ b/apps/web/src/components/editor/preview-panel.tsx
@@ -260,6 +260,7 @@ export function PreviewPanel() {
}, []);
const hasAnyElements = tracks.some((track) => track.elements.length > 0);
+ const shouldRenderPreview = hasAnyElements || activeProject?.backgroundType;
const getActiveElements = (): ActiveElement[] => {
const activeElements: ActiveElement[] = [];
@@ -701,7 +702,7 @@ export function PreviewPanel() {
className="flex-1 flex flex-col items-center justify-center min-h-0 min-w-0"
>
<div className="flex-1" />
- {hasAnyElements ? (
+ {shouldRenderPreview ? (
<div
ref={previewRef}
className="relative overflow-hidden border"
@@ -727,9 +728,7 @@ export function PreviewPanel() {
aria-label="Video preview canvas"
/>
{activeElements.length === 0 ? (
- <div className="absolute inset-0 flex items-center justify-center text-muted-foreground">
- No elements at current time
- </div>
+ <></>
) : (
activeElements.map((elementData) => renderElement(elementData))
)}