summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2025-08-09 11:02:01 +0200
committerMaze Winther <mazewinther@gmail.com>2025-08-09 11:02:01 +0200
commit54e990af26adb6e4ce467a0e9e511e52acc865e1 (patch)
tree7c945150a6c2d2fa30a568df1a4dbef1a455bcac
parenteffb6a86417affc5bca76cdaba78a1f98ae161d2 (diff)
downloadopencut-54e990af26adb6e4ce467a0e9e511e52acc865e1.tar.gz
Merge pull request [#514](https://github.com/mazeincoding/AppCut/issues/514)
-rw-r--r--apps/web/src/app/editor/[project_id]/page.tsx340
-rw-r--r--apps/web/src/components/editor-header.tsx2
-rw-r--r--apps/web/src/components/panel-preset-selector.tsx91
-rw-r--r--apps/web/src/stores/panel-store.ts212
4 files changed, 572 insertions, 73 deletions
diff --git a/apps/web/src/app/editor/[project_id]/page.tsx b/apps/web/src/app/editor/[project_id]/page.tsx
index cae8e779..4d5a27fc 100644
--- a/apps/web/src/app/editor/[project_id]/page.tsx
+++ b/apps/web/src/app/editor/[project_id]/page.tsx
@@ -30,6 +30,8 @@ export default function Editor() {
setTimeline,
propertiesPanel,
setPropertiesPanel,
+ activePreset,
+ resetCounter,
} = usePanelStore();
const {
@@ -153,72 +155,302 @@ export default function Editor() {
<div className="h-screen w-screen flex flex-col bg-background overflow-hidden">
<EditorHeader />
<div className="flex-1 min-h-0 min-w-0">
- <ResizablePanelGroup
- direction="vertical"
- className="h-full w-full gap-[0.18rem]"
- >
- <ResizablePanel
- defaultSize={mainContent}
- minSize={30}
- maxSize={85}
- onResize={setMainContent}
- className="min-h-0"
+ {activePreset === "media" ? (
+ <ResizablePanelGroup
+ key={`media-${activePreset}-${resetCounter}`}
+ direction="horizontal"
+ className="h-full w-full gap-[0.18rem] px-3 pb-3"
>
- {/* Main content area */}
- <ResizablePanelGroup
- direction="horizontal"
- className="h-full w-full gap-[0.19rem] px-3"
+ <ResizablePanel
+ defaultSize={toolsPanel}
+ minSize={15}
+ maxSize={40}
+ onResize={setToolsPanel}
+ className="min-w-0 rounded-sm"
>
- {/* Tools Panel */}
- <ResizablePanel
- defaultSize={toolsPanel}
- minSize={15}
- maxSize={40}
- onResize={setToolsPanel}
- className="min-w-0 rounded-sm"
+ <MediaPanel />
+ </ResizablePanel>
+
+ <ResizableHandle withHandle />
+
+ <ResizablePanel
+ defaultSize={100 - toolsPanel}
+ minSize={60}
+ className="min-w-0 min-h-0"
+ >
+ <ResizablePanelGroup
+ direction="vertical"
+ className="h-full w-full gap-[0.18rem]"
>
- <MediaPanel />
- </ResizablePanel>
+ <ResizablePanel
+ defaultSize={mainContent}
+ minSize={30}
+ maxSize={85}
+ onResize={setMainContent}
+ className="min-h-0"
+ >
+ <ResizablePanelGroup
+ direction="horizontal"
+ className="h-full w-full gap-[0.19rem]"
+ >
+ <ResizablePanel
+ defaultSize={previewPanel}
+ minSize={30}
+ onResize={setPreviewPanel}
+ className="min-w-0 min-h-0 flex-1"
+ >
+ <PreviewPanel />
+ </ResizablePanel>
+
+ <ResizableHandle withHandle />
- <ResizableHandle withHandle />
+ <ResizablePanel
+ defaultSize={propertiesPanel}
+ minSize={15}
+ maxSize={40}
+ onResize={setPropertiesPanel}
+ className="min-w-0"
+ >
+ <PropertiesPanel />
+ </ResizablePanel>
+ </ResizablePanelGroup>
+ </ResizablePanel>
- {/* Preview Area */}
- <ResizablePanel
- defaultSize={previewPanel}
- minSize={30}
- onResize={setPreviewPanel}
- className="min-w-0 min-h-0 flex-1"
+ <ResizableHandle withHandle />
+
+ <ResizablePanel
+ defaultSize={timeline}
+ minSize={15}
+ maxSize={70}
+ onResize={setTimeline}
+ className="min-h-0"
+ >
+ <Timeline />
+ </ResizablePanel>
+ </ResizablePanelGroup>
+ </ResizablePanel>
+ </ResizablePanelGroup>
+ ) : activePreset === "inspector" ? (
+ <ResizablePanelGroup
+ key={`inspector-${activePreset}-${resetCounter}`}
+ direction="horizontal"
+ className="h-full w-full gap-[0.18rem] px-3 pb-3"
+ >
+ <ResizablePanel
+ defaultSize={100 - propertiesPanel}
+ minSize={30}
+ onResize={(size) => setPropertiesPanel(100 - size)}
+ className="min-w-0 min-h-0"
+ >
+ <ResizablePanelGroup
+ direction="vertical"
+ className="h-full w-full gap-[0.18rem]"
>
- <PreviewPanel />
- </ResizablePanel>
+ <ResizablePanel
+ defaultSize={mainContent}
+ minSize={30}
+ maxSize={85}
+ onResize={setMainContent}
+ className="min-h-0"
+ >
+ <ResizablePanelGroup
+ direction="horizontal"
+ className="h-full w-full gap-[0.19rem]"
+ >
+ <ResizablePanel
+ defaultSize={toolsPanel}
+ minSize={15}
+ maxSize={40}
+ onResize={setToolsPanel}
+ className="min-w-0 rounded-sm"
+ >
+ <MediaPanel />
+ </ResizablePanel>
- <ResizableHandle withHandle />
+ <ResizableHandle withHandle />
- <ResizablePanel
- defaultSize={propertiesPanel}
- minSize={15}
- maxSize={40}
- onResize={setPropertiesPanel}
- className="min-w-0 rounded-sm"
+ <ResizablePanel
+ defaultSize={previewPanel}
+ minSize={30}
+ onResize={setPreviewPanel}
+ className="min-w-0 min-h-0 flex-1"
+ >
+ <PreviewPanel />
+ </ResizablePanel>
+ </ResizablePanelGroup>
+ </ResizablePanel>
+
+ <ResizableHandle withHandle />
+
+ <ResizablePanel
+ defaultSize={timeline}
+ minSize={15}
+ maxSize={70}
+ onResize={setTimeline}
+ className="min-h-0"
+ >
+ <Timeline />
+ </ResizablePanel>
+ </ResizablePanelGroup>
+ </ResizablePanel>
+
+ <ResizableHandle withHandle />
+
+ <ResizablePanel
+ defaultSize={propertiesPanel}
+ minSize={15}
+ maxSize={40}
+ onResize={setPropertiesPanel}
+ className="min-w-0 min-h-0"
+ >
+ <PropertiesPanel />
+ </ResizablePanel>
+ </ResizablePanelGroup>
+ ) : activePreset === "vertical-preview" ? (
+ <ResizablePanelGroup
+ key={`vertical-preview-${activePreset}-${resetCounter}`}
+ direction="horizontal"
+ className="h-full w-full gap-[0.18rem] px-3 pb-3"
+ >
+ <ResizablePanel
+ defaultSize={100 - previewPanel}
+ minSize={30}
+ onResize={(size) => setPreviewPanel(100 - size)}
+ className="min-w-0 min-h-0"
+ >
+ <ResizablePanelGroup
+ direction="vertical"
+ className="h-full w-full gap-[0.18rem]"
>
- <PropertiesPanel />
- </ResizablePanel>
- </ResizablePanelGroup>
- </ResizablePanel>
+ <ResizablePanel
+ defaultSize={mainContent}
+ minSize={30}
+ maxSize={85}
+ onResize={setMainContent}
+ className="min-h-0"
+ >
+ <ResizablePanelGroup
+ direction="horizontal"
+ className="h-full w-full gap-[0.19rem]"
+ >
+ <ResizablePanel
+ defaultSize={toolsPanel}
+ minSize={15}
+ maxSize={40}
+ onResize={setToolsPanel}
+ className="min-w-0 rounded-sm"
+ >
+ <MediaPanel />
+ </ResizablePanel>
- <ResizableHandle withHandle />
+ <ResizableHandle withHandle />
- {/* Timeline */}
- <ResizablePanel
- defaultSize={timeline}
- minSize={15}
- maxSize={70}
- onResize={setTimeline}
- className="min-h-0 px-3 pb-3"
+ <ResizablePanel
+ defaultSize={propertiesPanel}
+ minSize={15}
+ maxSize={40}
+ onResize={setPropertiesPanel}
+ className="min-w-0"
+ >
+ <PropertiesPanel />
+ </ResizablePanel>
+ </ResizablePanelGroup>
+ </ResizablePanel>
+
+ <ResizableHandle withHandle />
+
+ <ResizablePanel
+ defaultSize={timeline}
+ minSize={15}
+ maxSize={70}
+ onResize={setTimeline}
+ className="min-h-0"
+ >
+ <Timeline />
+ </ResizablePanel>
+ </ResizablePanelGroup>
+ </ResizablePanel>
+
+ <ResizableHandle withHandle />
+
+ <ResizablePanel
+ defaultSize={previewPanel}
+ minSize={30}
+ onResize={setPreviewPanel}
+ className="min-w-0 min-h-0"
+ >
+ <PreviewPanel />
+ </ResizablePanel>
+ </ResizablePanelGroup>
+ ) : (
+ <ResizablePanelGroup
+ key={`default-${activePreset}-${resetCounter}`}
+ direction="vertical"
+ className="h-full w-full gap-[0.18rem]"
>
- <Timeline />
- </ResizablePanel>
- </ResizablePanelGroup>
+ <ResizablePanel
+ defaultSize={mainContent}
+ minSize={30}
+ maxSize={85}
+ onResize={setMainContent}
+ className="min-h-0"
+ >
+ {/* Main content area */}
+ <ResizablePanelGroup
+ direction="horizontal"
+ className="h-full w-full gap-[0.19rem] px-3"
+ >
+ {/* Tools Panel */}
+ <ResizablePanel
+ defaultSize={toolsPanel}
+ minSize={15}
+ maxSize={40}
+ onResize={setToolsPanel}
+ className="min-w-0 rounded-sm"
+ >
+ <MediaPanel />
+ </ResizablePanel>
+
+ <ResizableHandle withHandle />
+
+ {/* Preview Area */}
+ <ResizablePanel
+ defaultSize={previewPanel}
+ minSize={30}
+ onResize={setPreviewPanel}
+ className="min-w-0 min-h-0 flex-1"
+ >
+ <PreviewPanel />
+ </ResizablePanel>
+
+ <ResizableHandle withHandle />
+
+ <ResizablePanel
+ defaultSize={propertiesPanel}
+ minSize={15}
+ maxSize={40}
+ onResize={setPropertiesPanel}
+ className="min-w-0 rounded-sm"
+ >
+ <PropertiesPanel />
+ </ResizablePanel>
+ </ResizablePanelGroup>
+ </ResizablePanel>
+
+ <ResizableHandle withHandle />
+
+ {/* Timeline */}
+ <ResizablePanel
+ defaultSize={timeline}
+ minSize={15}
+ maxSize={70}
+ onResize={setTimeline}
+ className="min-h-0 px-3 pb-3"
+ >
+ <Timeline />
+ </ResizablePanel>
+ </ResizablePanelGroup>
+ )}
</div>
<Onboarding />
</div>
diff --git a/apps/web/src/components/editor-header.tsx b/apps/web/src/components/editor-header.tsx
index 0caa6c3e..6db2543b 100644
--- a/apps/web/src/components/editor-header.tsx
+++ b/apps/web/src/components/editor-header.tsx
@@ -30,6 +30,7 @@ import { FaDiscord } from "react-icons/fa6";
import { useTheme } from "next-themes";
import { usePlaybackStore } from "@/stores/playback-store";
import { TransitionUpIcon } from "./icons";
+import { PanelPresetSelector } from "./panel-preset-selector";
export function EditorHeader() {
const { getTotalDuration } = useTimelineStore();
@@ -141,6 +142,7 @@ export function EditorHeader() {
const rightContent = (
<nav className="flex items-center gap-2">
+ <PanelPresetSelector />
<KeyboardShortcutsHelp />
<ExportButton />
<Button
diff --git a/apps/web/src/components/panel-preset-selector.tsx b/apps/web/src/components/panel-preset-selector.tsx
new file mode 100644
index 00000000..c5351f76
--- /dev/null
+++ b/apps/web/src/components/panel-preset-selector.tsx
@@ -0,0 +1,91 @@
+"use client";
+
+import { Button } from "./ui/button";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from "./ui/dropdown-menu";
+import { ChevronDown, RotateCcw, LayoutPanelTop } from "lucide-react";
+import { usePanelStore, type PanelPreset } from "@/stores/panel-store";
+
+const PRESET_LABELS: Record<PanelPreset, string> = {
+ default: "Default",
+ media: "Media",
+ inspector: "Inspector",
+ "vertical-preview": "Vertical Preview",
+};
+
+const PRESET_DESCRIPTIONS: Record<PanelPreset, string> = {
+ default: "Media, preview, and inspector on top row, timeline on bottom",
+ media: "Full height media on left, preview and inspector on top row",
+ inspector: "Full height inspector on right, media and preview on top row",
+ "vertical-preview": "Full height preview on right for vertical videos",
+};
+
+export function PanelPresetSelector() {
+ const { activePreset, setActivePreset, resetPreset } = usePanelStore();
+
+ const handlePresetChange = (preset: PanelPreset) => {
+ setActivePreset(preset);
+ };
+
+ const handleResetPreset = (preset: PanelPreset, event: React.MouseEvent) => {
+ event.stopPropagation();
+ resetPreset(preset);
+ };
+
+ return (
+ <DropdownMenu>
+ <DropdownMenuTrigger asChild>
+ <Button
+ variant="secondary"
+ size="sm"
+ className="h-8 px-2 flex items-center gap-1 text-xs"
+ title="Panel Presets"
+ >
+ <LayoutPanelTop className="h-4 w-4" />
+ <ChevronDown className="h-3 w-3" />
+ </Button>
+ </DropdownMenuTrigger>
+ <DropdownMenuContent align="start" className="w-64">
+ <div className="px-2 py-1.5 text-xs font-medium text-muted-foreground">
+ Panel Presets
+ </div>
+ <DropdownMenuSeparator />
+ {(Object.keys(PRESET_LABELS) as PanelPreset[]).map((preset) => (
+ <DropdownMenuItem
+ key={preset}
+ onClick={() => handlePresetChange(preset)}
+ className="flex items-start justify-between gap-2 py-2 cursor-pointer"
+ >
+ <div className="flex-1 min-w-0">
+ <div className="flex items-center gap-2">
+ <span className="font-medium text-sm">
+ {PRESET_LABELS[preset]}
+ </span>
+ {activePreset === preset && (
+ <div className="h-2 w-2 rounded-full bg-primary" />
+ )}
+ </div>
+ <p className="text-xs text-muted-foreground mt-0.5 leading-tight">
+ {PRESET_DESCRIPTIONS[preset]}
+ </p>
+ </div>
+ <Button
+ variant="secondary"
+ size="icon"
+ className="h-6 w-6 shrink-0 opacity-60 hover:opacity-100"
+ onClick={(e) => handleResetPreset(preset, e)}
+ title={`Reset ${PRESET_LABELS[preset]} preset`}
+ >
+ <RotateCcw className="h-3 w-3" />
+ </Button>
+ </DropdownMenuItem>
+ ))}
+ </DropdownMenuContent>
+ </DropdownMenu>
+ );
+}
diff --git a/apps/web/src/stores/panel-store.ts b/apps/web/src/stores/panel-store.ts
index ebbce3c0..ae09c13b 100644
--- a/apps/web/src/stores/panel-store.ts
+++ b/apps/web/src/stores/panel-store.ts
@@ -1,48 +1,222 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
-const DEFAULT_PANEL_SIZES = {
- toolsPanel: 45,
- previewPanel: 75,
- propertiesPanel: 20,
- mainContent: 70,
- timeline: 30,
-} as const;
+export type PanelPreset =
+ | "default"
+ | "media"
+ | "inspector"
+ | "vertical-preview";
-interface PanelState {
- // Panel sizes as percentages
+interface PanelSizes {
toolsPanel: number;
previewPanel: number;
propertiesPanel: number;
mainContent: number;
timeline: number;
+}
+
+export const PRESET_CONFIGS: Record<PanelPreset, PanelSizes> = {
+ default: {
+ toolsPanel: 25,
+ previewPanel: 50,
+ propertiesPanel: 25,
+ mainContent: 70,
+ timeline: 30,
+ },
+ media: {
+ toolsPanel: 30,
+ previewPanel: 45,
+ propertiesPanel: 25,
+ mainContent: 100,
+ timeline: 25,
+ },
+ inspector: {
+ toolsPanel: 30,
+ previewPanel: 70,
+ propertiesPanel: 30,
+ mainContent: 75,
+ timeline: 25,
+ },
+ "vertical-preview": {
+ toolsPanel: 30,
+ previewPanel: 40,
+ propertiesPanel: 30,
+ mainContent: 75,
+ timeline: 25,
+ },
+};
+
+interface PanelState extends PanelSizes {
+ activePreset: PanelPreset;
+ presetCustomSizes: Record<PanelPreset, Partial<PanelSizes>>;
+ resetCounter: number;
mediaViewMode: "grid" | "list";
- // Actions
setToolsPanel: (size: number) => void;
setPreviewPanel: (size: number) => void;
setPropertiesPanel: (size: number) => void;
setMainContent: (size: number) => void;
setTimeline: (size: number) => void;
setMediaViewMode: (mode: "grid" | "list") => void;
+
+ setActivePreset: (preset: PanelPreset) => void;
+ resetPreset: (preset: PanelPreset) => void;
+ getCurrentPresetSizes: () => PanelSizes;
}
export const usePanelStore = create<PanelState>()(
persist(
- (set) => ({
- // Default sizes - optimized for responsiveness
- ...DEFAULT_PANEL_SIZES,
+ (set, get) => ({
+ ...PRESET_CONFIGS.default,
+ activePreset: "default" as PanelPreset,
+ presetCustomSizes: {
+ default: {},
+ media: {},
+ inspector: {},
+ "vertical-preview": {},
+ },
+ resetCounter: 0,
mediaViewMode: "grid" as const,
- // Actions
- setToolsPanel: (size) => set({ toolsPanel: size }),
- setPreviewPanel: (size) => set({ previewPanel: size }),
- setPropertiesPanel: (size) => set({ propertiesPanel: size }),
- setMainContent: (size) => set({ mainContent: size }),
- setTimeline: (size) => set({ timeline: size }),
+ setToolsPanel: (size) => {
+ const { activePreset, presetCustomSizes } = get();
+ set({
+ toolsPanel: size,
+ presetCustomSizes: {
+ ...presetCustomSizes,
+ [activePreset]: {
+ ...presetCustomSizes[activePreset],
+ toolsPanel: size,
+ },
+ },
+ });
+ },
+ setPreviewPanel: (size) => {
+ const { activePreset, presetCustomSizes } = get();
+ set({
+ previewPanel: size,
+ presetCustomSizes: {
+ ...presetCustomSizes,
+ [activePreset]: {
+ ...presetCustomSizes[activePreset],
+ previewPanel: size,
+ },
+ },
+ });
+ },
+ setPropertiesPanel: (size) => {
+ const { activePreset, presetCustomSizes } = get();
+ set({
+ propertiesPanel: size,
+ presetCustomSizes: {
+ ...presetCustomSizes,
+ [activePreset]: {
+ ...presetCustomSizes[activePreset],
+ propertiesPanel: size,
+ },
+ },
+ });
+ },
+ setMainContent: (size) => {
+ const { activePreset, presetCustomSizes } = get();
+ set({
+ mainContent: size,
+ presetCustomSizes: {
+ ...presetCustomSizes,
+ [activePreset]: {
+ ...presetCustomSizes[activePreset],
+ mainContent: size,
+ },
+ },
+ });
+ },
+ setTimeline: (size) => {
+ const { activePreset, presetCustomSizes } = get();
+ set({
+ timeline: size,
+ presetCustomSizes: {
+ ...presetCustomSizes,
+ [activePreset]: {
+ ...presetCustomSizes[activePreset],
+ timeline: size,
+ },
+ },
+ });
+ },
setMediaViewMode: (mode) => set({ mediaViewMode: mode }),
+
+ setActivePreset: (preset) => {
+ const {
+ activePreset: currentPreset,
+ presetCustomSizes,
+ toolsPanel,
+ previewPanel,
+ propertiesPanel,
+ mainContent,
+ timeline,
+ } = get();
+
+ const updatedPresetCustomSizes = {
+ ...presetCustomSizes,
+ [currentPreset]: {
+ toolsPanel,
+ previewPanel,
+ propertiesPanel,
+ mainContent,
+ timeline,
+ },
+ };
+
+ const defaultSizes = PRESET_CONFIGS[preset];
+ const customSizes = updatedPresetCustomSizes[preset] || {};
+ const finalSizes = { ...defaultSizes, ...customSizes } as PanelSizes;
+
+ set({
+ activePreset: preset,
+ presetCustomSizes: updatedPresetCustomSizes,
+ ...finalSizes,
+ });
+ },
+
+ resetPreset: (preset) => {
+ const { presetCustomSizes, activePreset, resetCounter } = get();
+ const defaultSizes = PRESET_CONFIGS[preset];
+
+ const newPresetCustomSizes = {
+ ...presetCustomSizes,
+ [preset]: {},
+ };
+
+ const updates: Partial<PanelState> = {
+ presetCustomSizes: newPresetCustomSizes,
+ resetCounter: resetCounter + 1,
+ };
+
+ if (preset === activePreset) {
+ Object.assign(updates, defaultSizes);
+ }
+
+ set(updates);
+ },
+
+ getCurrentPresetSizes: () => {
+ const {
+ toolsPanel,
+ previewPanel,
+ propertiesPanel,
+ mainContent,
+ timeline,
+ } = get();
+ return {
+ toolsPanel,
+ previewPanel,
+ propertiesPanel,
+ mainContent,
+ timeline,
+ };
+ },
}),
{
name: "panel-sizes",