From 81de82ffaa3181c94281a5a810a0f671bd2a8d99 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Mon, 4 Aug 2025 10:37:40 +0200 Subject: feat: new pattern-craft color --- apps/web/src/data/colors/pattern-craft.ts | 3 +++ 1 file changed, 3 insertions(+) (limited to 'apps/web') diff --git a/apps/web/src/data/colors/pattern-craft.ts b/apps/web/src/data/colors/pattern-craft.ts index 56c49b34..f7a5fc5b 100644 --- a/apps/web/src/data/colors/pattern-craft.ts +++ b/apps/web/src/data/colors/pattern-craft.ts @@ -27,4 +27,7 @@ export const patternCraftGradients = [ // Purple Glow Left "radial-gradient(circle at top left, rgba(173, 109, 244, 0.5), transparent 70%)", + + // Pastel Wave + "linear-gradient(120deg, #d5c5ff 0%, #a7f3d0 50%, #f0f0f0 100%)", ]; -- cgit v1.3.1 From b0cfedbc5aacdaf4df165ef4b0a61c3128aff1f2 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Mon, 4 Aug 2025 10:59:55 +0200 Subject: refactor: remove unnecessary code from tab-bar --- .../src/components/editor/media-panel/tabbar.tsx | 99 +--------------------- 1 file changed, 4 insertions(+), 95 deletions(-) (limited to 'apps/web') diff --git a/apps/web/src/components/editor/media-panel/tabbar.tsx b/apps/web/src/components/editor/media-panel/tabbar.tsx index fc4f677d..8d2e3850 100644 --- a/apps/web/src/components/editor/media-panel/tabbar.tsx +++ b/apps/web/src/components/editor/media-panel/tabbar.tsx @@ -2,82 +2,22 @@ import { cn } from "@/lib/utils"; import { Tab, tabs, useMediaPanelStore } from "./store"; -import { Button } from "@/components/ui/button"; -import { ChevronRight, ChevronLeft } from "lucide-react"; -import { useRef, useState, useEffect } from "react"; export function TabBar() { const { activeTab, setActiveTab } = useMediaPanelStore(); - const scrollContainerRef = useRef(null); - const [isAtEnd, setIsAtEnd] = useState(false); - const [isAtStart, setIsAtStart] = useState(true); - - const scrollToEnd = () => { - if (scrollContainerRef.current) { - scrollContainerRef.current.scrollTo({ - left: scrollContainerRef.current.scrollWidth, - }); - setIsAtEnd(true); - setIsAtStart(false); - } - }; - - const scrollToStart = () => { - if (scrollContainerRef.current) { - scrollContainerRef.current.scrollTo({ - left: 0, - }); - setIsAtStart(true); - setIsAtEnd(false); - } - }; - - const checkScrollPosition = () => { - if (scrollContainerRef.current) { - const { scrollLeft, scrollWidth, clientWidth } = - scrollContainerRef.current; - const isAtEndNow = scrollLeft + clientWidth >= scrollWidth - 1; - const isAtStartNow = scrollLeft <= 1; - setIsAtEnd(isAtEndNow); - setIsAtStart(isAtStartNow); - } - }; - - // We're using useEffect because we need to sync with external DOM scroll events - useEffect(() => { - const container = scrollContainerRef.current; - if (!container) return; - - checkScrollPosition(); - container.addEventListener("scroll", checkScrollPosition); - - const resizeObserver = new ResizeObserver(checkScrollPosition); - resizeObserver.observe(container); - - return () => { - container.removeEventListener("scroll", checkScrollPosition); - resizeObserver.disconnect(); - }; - }, []); return (
- -
+
{(Object.keys(tabs) as Tab[]).map((tabKey) => { const tab = tabs[tabKey]; return (
setActiveTab(tabKey)} key={tabKey} @@ -87,37 +27,6 @@ export function TabBar() { ); })}
- -
- ); -} - -function ScrollButton({ - direction, - onClick, - isVisible, -}: { - direction: "left" | "right"; - onClick: () => void; - isVisible: boolean; -}) { - if (!isVisible) return null; - - const Icon = direction === "left" ? ChevronLeft : ChevronRight; - - return ( -
-
); } -- cgit v1.3.1 From efa5be2ab8fd5be095995378caddd0784fc3fbd3 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Mon, 4 Aug 2025 11:12:44 +0200 Subject: feat: proper light mode for hero section --- apps/web/public/landing-page-bg.png | Bin 230853 -> 0 bytes apps/web/public/landing-page-dark.png | Bin 0 -> 223614 bytes apps/web/public/landing-page-light.png | Bin 0 -> 283708 bytes apps/web/src/components/header.tsx | 4 ++-- apps/web/src/components/landing/handlebars.tsx | 4 ++-- apps/web/src/components/landing/hero.tsx | 2 +- apps/web/src/components/ui/sponsor-button.tsx | 8 ++++---- 7 files changed, 9 insertions(+), 9 deletions(-) delete mode 100644 apps/web/public/landing-page-bg.png create mode 100644 apps/web/public/landing-page-dark.png create mode 100644 apps/web/public/landing-page-light.png (limited to 'apps/web') diff --git a/apps/web/public/landing-page-bg.png b/apps/web/public/landing-page-bg.png deleted file mode 100644 index 4f873945..00000000 Binary files a/apps/web/public/landing-page-bg.png and /dev/null differ diff --git a/apps/web/public/landing-page-dark.png b/apps/web/public/landing-page-dark.png new file mode 100644 index 00000000..1a33bb29 Binary files /dev/null and b/apps/web/public/landing-page-dark.png differ diff --git a/apps/web/public/landing-page-light.png b/apps/web/public/landing-page-light.png new file mode 100644 index 00000000..348886db Binary files /dev/null and b/apps/web/public/landing-page-light.png differ diff --git a/apps/web/src/components/header.tsx b/apps/web/src/components/header.tsx index f5f847be..bed04a27 100644 --- a/apps/web/src/components/header.tsx +++ b/apps/web/src/components/header.tsx @@ -9,7 +9,7 @@ import Image from "next/image"; export function Header() { const leftContent = ( - OpenCut Logo + OpenCut Logo OpenCut ); @@ -40,7 +40,7 @@ export function Header() { return (
diff --git a/apps/web/src/components/landing/handlebars.tsx b/apps/web/src/components/landing/handlebars.tsx index 3ed39a9f..758717e5 100644 --- a/apps/web/src/components/landing/handlebars.tsx +++ b/apps/web/src/components/landing/handlebars.tsx @@ -50,7 +50,7 @@ export function Handlebars({ children }: HandlebarsProps) {
- + Sponsored by
-
+
- + {companyName}
-- cgit v1.3.1 From 0386624ff5fd6d82d64d6b103ddc1a7e5045f45b Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Mon, 4 Aug 2025 12:35:14 +0200 Subject: fix: click empty area on timeline to move playhead --- apps/web/src/components/editor/timeline/index.tsx | 117 +++++++++++++++++++--- 1 file changed, 102 insertions(+), 15 deletions(-) (limited to 'apps/web') diff --git a/apps/web/src/components/editor/timeline/index.tsx b/apps/web/src/components/editor/timeline/index.tsx index 7a2cc7f2..dd81b7b3 100644 --- a/apps/web/src/components/editor/timeline/index.tsx +++ b/apps/web/src/components/editor/timeline/index.tsx @@ -167,11 +167,32 @@ export function Timeline() { const handleTimelineMouseDown = useCallback((e: React.MouseEvent) => { // Only track mouse down on timeline background areas (not elements) const target = e.target as HTMLElement; + console.log( + JSON.stringify({ + debug_mousedown: "START", + target_class: target.className, + target_parent_class: target.parentElement?.className, + clientX: e.clientX, + clientY: e.clientY, + timeStamp: e.timeStamp, + }) + ); + const isTimelineBackground = !target.closest(".timeline-element") && !playheadRef.current?.contains(target) && !target.closest("[data-track-labels]"); + console.log( + JSON.stringify({ + debug_mousedown: "CHECK", + isTimelineBackground, + hasTimelineElement: !!target.closest(".timeline-element"), + hasPlayhead: !!playheadRef.current?.contains(target), + hasTrackLabels: !!target.closest("[data-track-labels]"), + }) + ); + if (isTimelineBackground) { mouseTrackingRef.current = { isMouseDown: true, @@ -179,12 +200,38 @@ export function Timeline() { downY: e.clientY, downTime: e.timeStamp, }; + console.log( + JSON.stringify({ + debug_mousedown: "TRACKED", + mouseTracking: mouseTrackingRef.current, + }) + ); + } else { + console.log( + JSON.stringify({ + debug_mousedown: "IGNORED - not timeline background", + }) + ); } }, []); // Timeline content click to seek handler const handleTimelineContentClick = useCallback( (e: React.MouseEvent) => { + console.log( + JSON.stringify({ + debug_click: "START", + target: (e.target as HTMLElement).className, + target_parent: (e.target as HTMLElement).parentElement?.className, + mouseTracking: mouseTrackingRef.current, + isSelecting, + justFinishedSelecting, + clickX: e.clientX, + clickY: e.clientY, + timeStamp: e.timeStamp, + }) + ); + const { isMouseDown, downX, downY, downTime } = mouseTrackingRef.current; // Reset mouse tracking @@ -199,8 +246,8 @@ export function Timeline() { if (!isMouseDown) { console.log( JSON.stringify({ - ignoredClickWithoutMouseDown: true, - timeStamp: e.timeStamp, + debug_click: "REJECTED - no mousedown", + mouseTracking: mouseTrackingRef.current, }) ); return; @@ -214,11 +261,10 @@ export function Timeline() { if (deltaX > 5 || deltaY > 5 || deltaTime > 500) { console.log( JSON.stringify({ - ignoredDragNotClick: true, + debug_click: "REJECTED - movement too large", deltaX, deltaY, deltaTime, - timeStamp: e.timeStamp, }) ); return; @@ -226,27 +272,54 @@ export function Timeline() { // Don't seek if this was a selection box operation if (isSelecting || justFinishedSelecting) { + console.log( + JSON.stringify({ + debug_click: "REJECTED - selection operation", + isSelecting, + justFinishedSelecting, + }) + ); return; } // Don't seek if clicking on timeline elements, but still deselect if ((e.target as HTMLElement).closest(".timeline-element")) { + console.log( + JSON.stringify({ + debug_click: "REJECTED - clicked timeline element", + }) + ); return; } // Don't seek if clicking on playhead if (playheadRef.current?.contains(e.target as Node)) { + console.log( + JSON.stringify({ + debug_click: "REJECTED - clicked playhead", + }) + ); return; } // Don't seek if clicking on track labels if ((e.target as HTMLElement).closest("[data-track-labels]")) { + console.log( + JSON.stringify({ + debug_click: "REJECTED - clicked track labels", + }) + ); clearSelectedElements(); return; } // Clear selected elements when clicking empty timeline area - console.log(JSON.stringify({ clearingSelectedElements: true })); + console.log( + JSON.stringify({ + debug_click: "PROCEEDING - clearing elements", + clearingSelectedElements: true, + }) + ); clearSelectedElements(); // Determine if we're clicking in ruler or tracks area @@ -254,24 +327,39 @@ export function Timeline() { "[data-ruler-area]" ); + console.log( + JSON.stringify({ + debug_click: "CALCULATING POSITION", + isRulerClick, + clientX: e.clientX, + clientY: e.clientY, + target_element: (e.target as HTMLElement).tagName, + target_class: (e.target as HTMLElement).className, + }) + ); + let mouseX: number; let scrollLeft = 0; if (isRulerClick) { // Calculate based on ruler position - const rulerContent = rulerScrollRef.current?.querySelector( - "[data-radix-scroll-area-viewport]" - ) as HTMLElement; - if (!rulerContent) return; + const rulerContent = rulerScrollRef.current; + if (!rulerContent) { + console.log( + JSON.stringify({ + debug_click: "ERROR - no ruler container found", + }) + ); + return; + } const rect = rulerContent.getBoundingClientRect(); mouseX = e.clientX - rect.left; scrollLeft = rulerContent.scrollLeft; } else { - // Calculate based on tracks content position - const tracksContent = tracksScrollRef.current?.querySelector( - "[data-radix-scroll-area-viewport]" - ) as HTMLElement; - if (!tracksContent) return; + const tracksContent = tracksScrollRef.current; + if (!tracksContent) { + return; + } const rect = tracksContent.getBoundingClientRect(); mouseX = e.clientX - rect.left; scrollLeft = tracksContent.scrollLeft; @@ -289,7 +377,6 @@ export function Timeline() { // Use frame snapping for timeline clicking const projectFps = activeProject?.fps || 30; const time = snapTimeToFrame(rawTime, projectFps); - seek(time); }, [ -- cgit v1.3.1 From 2fcc5e0643a36c040343bc37249e7acbd6caf6de Mon Sep 17 00:00:00 2001 From: Pratiyank Kumar <159152851+Pratiyankkumar@users.noreply.github.com> Date: Mon, 4 Aug 2025 17:46:25 +0530 Subject: feat: add tooltip in TabBar (#515) --- .../src/components/editor/media-panel/tabbar.tsx | 21 ++++-- apps/web/src/components/ui/tooltip.tsx | 78 ++++++++++++++++------ 2 files changed, 75 insertions(+), 24 deletions(-) (limited to 'apps/web') diff --git a/apps/web/src/components/editor/media-panel/tabbar.tsx b/apps/web/src/components/editor/media-panel/tabbar.tsx index 8d2e3850..4c3965c4 100644 --- a/apps/web/src/components/editor/media-panel/tabbar.tsx +++ b/apps/web/src/components/editor/media-panel/tabbar.tsx @@ -2,6 +2,12 @@ import { cn } from "@/lib/utils"; import { Tab, tabs, useMediaPanelStore } from "./store"; +import { Button } from "@/components/ui/button"; +import { ChevronRight, ChevronLeft } from "lucide-react"; +import { useRef, useState, useEffect } from "react"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; + + export function TabBar() { const { activeTab, setActiveTab } = useMediaPanelStore(); @@ -14,15 +20,20 @@ export function TabBar() { return (
setActiveTab(tabKey)} key={tabKey} > - + + + + + +
{tab.label}
+
+
); })} diff --git a/apps/web/src/components/ui/tooltip.tsx b/apps/web/src/components/ui/tooltip.tsx index 4971e262..bfb75e2e 100644 --- a/apps/web/src/components/ui/tooltip.tsx +++ b/apps/web/src/components/ui/tooltip.tsx @@ -1,9 +1,8 @@ -"use client"; +import { cva, type VariantProps } from 'class-variance-authority'; +import { Tooltip as TooltipPrimitive } from 'radix-ui'; +import * as React from 'react'; -import * as React from "react"; -import { Tooltip as TooltipPrimitive } from "radix-ui"; - -import { cn } from "../../lib/utils"; +import { cn } from '@/lib/utils'; const TooltipProvider = TooltipPrimitive.Provider; @@ -11,22 +10,63 @@ const Tooltip = TooltipPrimitive.Root; const TooltipTrigger = TooltipPrimitive.Trigger; +const tooltipVariants = cva( + 'z-50 overflow-visible rounded-sm text-sm shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2', + { + variants: { + variant: { + default: 'bg-popover text-popover-foreground border px-3 py-1.5', + destructive: + 'bg-destructive/10 text-destructive dark:bg-destructive/20 border-destructive [border-width:0.5px]', + outline: 'border-border', + important: + 'bg-amber-100/90 text-amber-900 dark:bg-amber-900/20 dark:text-amber-300 border-amber-900 [border-width:0.5px]', + promotions: + 'bg-red-100/90 text-red-900 dark:bg-red-900/20 dark:text-red-300 border-red-900 [border-width:0.5px]', + personal: + 'bg-green-100/90 text-green-900 dark:bg-green-900/20 dark:text-green-300 border-green-900 [border-width:0.5px]', + updates: + 'bg-purple-100/90 text-purple-900 dark:bg-purple-900/20 dark:text-purple-300 border-purple-900 [border-width:0.5px]', + forums: + 'bg-blue-100/90 text-blue-900 dark:bg-blue-900/20 dark:text-blue-300 border-blue-900 [border-width:0.5px]', + sidebar: 'bg-white dark:bg-[#413F3E] p-2.5 flex flex-col gap-2', + }, + }, + defaultVariants: { + variant: 'default', + }, + }, +); + +interface TooltipContentProps + extends React.ComponentPropsWithoutRef, + VariantProps {} + const TooltipContent = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, sideOffset = 4, ...props }, ref) => ( - - - + TooltipContentProps +>(({ className, sideOffset = 4, variant, ...props }, ref) => ( + + {variant === 'sidebar' && ( + + + + )} + {props.children} + )); TooltipContent.displayName = TooltipPrimitive.Content.displayName; -export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }; +export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }; \ No newline at end of file -- cgit v1.3.1 From 9ec5a022520aa3a2d1e18b4e3b4056f2ff5dcd35 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Mon, 4 Aug 2025 17:17:48 +0200 Subject: feat: layout guide for tiktok --- .../public/platform-guides/tiktok-blueprint.png | Bin 0 -> 32525 bytes .../src/components/editor/layout-guide-overlay.tsx | 27 ++++++++ apps/web/src/components/editor/preview-panel.tsx | 68 ++++++++++++++++++++- apps/web/src/components/icons.tsx | 30 +++++++++ apps/web/src/stores/editor-store.ts | 32 ++++++++++ 5 files changed, 155 insertions(+), 2 deletions(-) create mode 100644 apps/web/public/platform-guides/tiktok-blueprint.png create mode 100644 apps/web/src/components/editor/layout-guide-overlay.tsx (limited to 'apps/web') diff --git a/apps/web/public/platform-guides/tiktok-blueprint.png b/apps/web/public/platform-guides/tiktok-blueprint.png new file mode 100644 index 00000000..3477a188 Binary files /dev/null and b/apps/web/public/platform-guides/tiktok-blueprint.png differ diff --git a/apps/web/src/components/editor/layout-guide-overlay.tsx b/apps/web/src/components/editor/layout-guide-overlay.tsx new file mode 100644 index 00000000..ff4cde57 --- /dev/null +++ b/apps/web/src/components/editor/layout-guide-overlay.tsx @@ -0,0 +1,27 @@ +"use client"; + +import { useEditorStore } from "@/stores/editor-store"; +import Image from "next/image"; + +function TikTokGuide() { + return ( +
+ TikTok layout guide +
+ ); +} + +export function LayoutGuideOverlay() { + const { layoutGuide } = useEditorStore(); + + if (layoutGuide.platform === null) return null; + if (layoutGuide.platform === "tiktok") return ; + + return null; +} diff --git a/apps/web/src/components/editor/preview-panel.tsx b/apps/web/src/components/editor/preview-panel.tsx index b9c03382..f4cd2016 100644 --- a/apps/web/src/components/editor/preview-panel.tsx +++ b/apps/web/src/components/editor/preview-panel.tsx @@ -8,7 +8,14 @@ import { useEditorStore } from "@/stores/editor-store"; import { VideoPlayer } from "@/components/ui/video-player"; import { AudioPlayer } from "@/components/ui/audio-player"; import { Button } from "@/components/ui/button"; -import { Play, Pause, Expand, SkipBack, SkipForward } from "lucide-react"; +import { + Play, + Pause, + Expand, + SkipBack, + SkipForward, + LayoutGrid, +} from "lucide-react"; import { useState, useRef, useEffect, useCallback } from "react"; import { cn } from "@/lib/utils"; import { formatTimeCode } from "@/lib/time"; @@ -16,6 +23,16 @@ import { EditableTimecode } from "@/components/ui/editable-timecode"; import { FONT_CLASS_MAP } from "@/lib/font-config"; import { useProjectStore } from "@/stores/project-store"; import { TextElementDragState } from "@/types/editor"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { Checkbox } from "@/components/ui/checkbox"; +import { LayoutGuideOverlay } from "./layout-guide-overlay"; +import { Label } from "../ui/label"; +import { SocialsIcon } from "../icons"; +import { PLATFORM_LAYOUTS, type PlatformLayout } from "@/stores/editor-store"; interface ActiveElement { element: TimelineElement; @@ -496,6 +513,7 @@ export function PreviewPanel() { renderElement(elementData, index) ) )} + {activeProject?.backgroundType === "blur" && blurBackgroundElements.length === 0 && activeElements.length > 0 && ( @@ -758,6 +776,7 @@ function FullscreenPreview({ renderElement(elementData, index) ) )} + {activeProject?.backgroundType === "blur" && blurBackgroundElements.length === 0 && activeElements.length > 0 && ( @@ -799,6 +818,7 @@ function PreviewToolbar({ getTotalDuration: () => number; }) { const { isPlaying } = usePlaybackStore(); + const { layoutGuide, toggleLayoutGuide } = useEditorStore(); if (isExpanded) { return ( @@ -818,9 +838,53 @@ function PreviewToolbar({ return (
+ + + + + +
+
+

Layout guide

+

+ Show platform-specific layout guides to help align your + content with interface elements like profile pictures, + usernames, and interaction buttons. +

+
+
+
+ toggleLayoutGuide(layoutGuide.platform || "tiktok")} + /> + +
+ {Object.entries(PLATFORM_LAYOUTS).map(([platform, label]) => ( +
+ toggleLayoutGuide(platform as PlatformLayout)} + /> + +
+ ))} +
+
+
+
- + -- cgit v1.3.1 From f0dd30ebfc16a61c2869da8407101e8533e584f0 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Mon, 4 Aug 2025 20:40:09 +0200 Subject: fix: tiktok blueprint --- .../web/public/platform-guides/tiktok-blueprint.png | Bin 32525 -> 32584 bytes 1 file changed, 0 insertions(+), 0 deletions(-) (limited to 'apps/web') diff --git a/apps/web/public/platform-guides/tiktok-blueprint.png b/apps/web/public/platform-guides/tiktok-blueprint.png index 3477a188..e87eb36d 100644 Binary files a/apps/web/public/platform-guides/tiktok-blueprint.png and b/apps/web/public/platform-guides/tiktok-blueprint.png differ -- cgit v1.3.1 From b0329f1372f10b887ad056b9220bf49a4e822e0a Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Mon, 4 Aug 2025 20:45:48 +0200 Subject: refactor: move canvas size to be a project-level setting instead of editor-level --- .../editor/media-panel/views/settings.tsx | 10 +- apps/web/src/lib/editor-utils.ts | 46 +++++++ apps/web/src/stores/editor-store.ts | 141 +++++++-------------- apps/web/src/stores/project-store.ts | 53 ++++++-- apps/web/src/stores/timeline-store.ts | 10 +- apps/web/src/types/editor.ts | 2 + apps/web/src/types/project.ts | 10 ++ 7 files changed, 159 insertions(+), 113 deletions(-) create mode 100644 apps/web/src/lib/editor-utils.ts (limited to 'apps/web') diff --git a/apps/web/src/components/editor/media-panel/views/settings.tsx b/apps/web/src/components/editor/media-panel/views/settings.tsx index 3bce1753..b6b79b59 100644 --- a/apps/web/src/components/editor/media-panel/views/settings.tsx +++ b/apps/web/src/components/editor/media-panel/views/settings.tsx @@ -57,14 +57,18 @@ function ProjectSettingsTabs() { } function ProjectInfoView() { - const { activeProject, updateProjectFps } = useProjectStore(); - const { canvasPresets, setCanvasSize } = useEditorStore(); + const { activeProject, updateProjectFps, updateCanvasSize } = + useProjectStore(); + const { canvasPresets } = useEditorStore(); const { getDisplayName } = useAspectRatio(); const handleAspectRatioChange = (value: string) => { const preset = canvasPresets.find((p) => p.name === value); if (preset) { - setCanvasSize({ width: preset.width, height: preset.height }); + updateCanvasSize( + { width: preset.width, height: preset.height }, + "preset" + ); } }; diff --git a/apps/web/src/lib/editor-utils.ts b/apps/web/src/lib/editor-utils.ts new file mode 100644 index 00000000..f041aa5e --- /dev/null +++ b/apps/web/src/lib/editor-utils.ts @@ -0,0 +1,46 @@ +import { CanvasSize } from "@/types/editor"; + +const DEFAULT_CANVAS_PRESETS = [ + { name: "16:9", width: 1920, height: 1080 }, + { name: "9:16", width: 1080, height: 1920 }, + { name: "1:1", width: 1080, height: 1080 }, + { name: "4:3", width: 1440, height: 1080 }, +]; + +/** + * Helper function to find the best matching canvas preset for an aspect ratio + * @param aspectRatio The target aspect ratio to match + * @returns The best matching canvas size + */ +export function findBestCanvasPreset(aspectRatio: number): CanvasSize { + // Calculate aspect ratio for each preset and find the closest match + let bestMatch = DEFAULT_CANVAS_PRESETS[0]; // Default to 16:9 HD + let smallestDifference = Math.abs( + aspectRatio - bestMatch.width / bestMatch.height + ); + + for (const preset of DEFAULT_CANVAS_PRESETS) { + const presetAspectRatio = preset.width / preset.height; + const difference = Math.abs(aspectRatio - presetAspectRatio); + + if (difference < smallestDifference) { + smallestDifference = difference; + bestMatch = preset; + } + } + + // If the difference is still significant (> 0.1), create a custom size + // based on the media aspect ratio with a reasonable resolution + const bestAspectRatio = bestMatch.width / bestMatch.height; + if (Math.abs(aspectRatio - bestAspectRatio) > 0.1) { + // Create custom dimensions based on the aspect ratio + if (aspectRatio > 1) { + // Landscape - use 1920 width + return { width: 1920, height: Math.round(1920 / aspectRatio) }; + } + // Portrait or square - use 1080 height + return { width: Math.round(1080 * aspectRatio), height: 1080 }; + } + + return { width: bestMatch.width, height: bestMatch.height }; +} diff --git a/apps/web/src/stores/editor-store.ts b/apps/web/src/stores/editor-store.ts index a9c8f2ca..9923aa87 100644 --- a/apps/web/src/stores/editor-store.ts +++ b/apps/web/src/stores/editor-store.ts @@ -1,7 +1,7 @@ import { create } from "zustand"; -import { CanvasSize, CanvasPreset } from "@/types/editor"; +import { persist } from "zustand/middleware"; +import { CanvasPreset } from "@/types/editor"; -type CanvasMode = "preset" | "original" | "custom"; export type PlatformLayout = "tiktok"; export const PLATFORM_LAYOUTS: Record = { @@ -17,9 +17,7 @@ interface EditorState { isInitializing: boolean; isPanelsReady: boolean; - // Canvas/Project settings - canvasSize: CanvasSize; - canvasMode: CanvasMode; + // Editor UI settings canvasPresets: CanvasPreset[]; layoutGuide: LayoutGuideSettings; @@ -27,9 +25,6 @@ interface EditorState { setInitializing: (loading: boolean) => void; setPanelsReady: (ready: boolean) => void; initializeApp: () => Promise; - setCanvasSize: (size: CanvasSize) => void; - setCanvasSizeToOriginal: (aspectRatio: number) => void; - setCanvasSizeFromAspectRatio: (aspectRatio: number) => void; setLayoutGuide: (settings: Partial) => void; toggleLayoutGuide: (platform: PlatformLayout) => void; } @@ -41,96 +36,56 @@ const DEFAULT_CANVAS_PRESETS: CanvasPreset[] = [ { name: "4:3", width: 1440, height: 1080 }, ]; -// Helper function to find the best matching canvas preset for an aspect ratio -const findBestCanvasPreset = (aspectRatio: number): CanvasSize => { - // Calculate aspect ratio for each preset and find the closest match - let bestMatch = DEFAULT_CANVAS_PRESETS[0]; // Default to 16:9 HD - let smallestDifference = Math.abs( - aspectRatio - bestMatch.width / bestMatch.height - ); - - for (const preset of DEFAULT_CANVAS_PRESETS) { - const presetAspectRatio = preset.width / preset.height; - const difference = Math.abs(aspectRatio - presetAspectRatio); - - if (difference < smallestDifference) { - smallestDifference = difference; - bestMatch = preset; - } - } - - // If the difference is still significant (> 0.1), create a custom size - // based on the media aspect ratio with a reasonable resolution - const bestAspectRatio = bestMatch.width / bestMatch.height; - if (Math.abs(aspectRatio - bestAspectRatio) > 0.1) { - // Create custom dimensions based on the aspect ratio - if (aspectRatio > 1) { - // Landscape - use 1920 width - return { width: 1920, height: Math.round(1920 / aspectRatio) }; - } - // Portrait or square - use 1080 height - return { width: Math.round(1080 * aspectRatio), height: 1080 }; - } - - return { width: bestMatch.width, height: bestMatch.height }; -}; - -export const useEditorStore = create((set, get) => ({ - // Initial states - isInitializing: true, - isPanelsReady: false, - canvasSize: { width: 1920, height: 1080 }, // Default 16:9 HD - canvasMode: "preset" as CanvasMode, - canvasPresets: DEFAULT_CANVAS_PRESETS, - layoutGuide: { - platform: null, - }, - - // Actions - setInitializing: (loading) => { - set({ isInitializing: loading }); - }, - - setPanelsReady: (ready) => { - set({ isPanelsReady: ready }); - }, - - initializeApp: async () => { - console.log("Initializing video editor..."); - set({ isInitializing: true, isPanelsReady: false }); +export const useEditorStore = create()( + persist( + (set) => ({ + // Initial states + isInitializing: true, + isPanelsReady: false, + canvasPresets: DEFAULT_CANVAS_PRESETS, + layoutGuide: { + platform: null, + }, - set({ isPanelsReady: true, isInitializing: false }); - console.log("Video editor ready"); - }, + // Actions + setInitializing: (loading) => { + set({ isInitializing: loading }); + }, - setCanvasSize: (size) => { - set({ canvasSize: size, canvasMode: "preset" }); - }, + setPanelsReady: (ready) => { + set({ isPanelsReady: ready }); + }, - setCanvasSizeToOriginal: (aspectRatio) => { - const newCanvasSize = findBestCanvasPreset(aspectRatio); - set({ canvasSize: newCanvasSize, canvasMode: "original" }); - }, + initializeApp: async () => { + console.log("Initializing video editor..."); + set({ isInitializing: true, isPanelsReady: false }); - setCanvasSizeFromAspectRatio: (aspectRatio) => { - const newCanvasSize = findBestCanvasPreset(aspectRatio); - set({ canvasSize: newCanvasSize, canvasMode: "custom" }); - }, + set({ isPanelsReady: true, isInitializing: false }); + console.log("Video editor ready"); + }, - setLayoutGuide: (settings) => { - set((state) => ({ - layoutGuide: { - ...state.layoutGuide, - ...settings, + setLayoutGuide: (settings) => { + set((state) => ({ + layoutGuide: { + ...state.layoutGuide, + ...settings, + }, + })); }, - })); - }, - toggleLayoutGuide: (platform) => { - set((state) => ({ - layoutGuide: { - platform: state.layoutGuide.platform === platform ? null : platform, + toggleLayoutGuide: (platform) => { + set((state) => ({ + layoutGuide: { + platform: state.layoutGuide.platform === platform ? null : platform, + }, + })); }, - })); - }, -})); + }), + { + name: "editor-settings", + partialize: (state) => ({ + layoutGuide: state.layoutGuide, + }), + } + ) +); diff --git a/apps/web/src/stores/project-store.ts b/apps/web/src/stores/project-store.ts index 5c617dff..db6d014a 100644 --- a/apps/web/src/stores/project-store.ts +++ b/apps/web/src/stores/project-store.ts @@ -5,6 +5,22 @@ import { toast } from "sonner"; import { useMediaStore } from "./media-store"; import { useTimelineStore } from "./timeline-store"; import { generateUUID } from "@/lib/utils"; +import { CanvasSize, CanvasMode } from "@/types/editor"; + +const DEFAULT_PROJECT: TProject = { + id: generateUUID(), + name: "Untitled", + thumbnail: "", + createdAt: new Date(), + updatedAt: new Date(), + backgroundColor: "#000000", + backgroundType: "color", + blurIntensity: 8, + bookmarks: [], + fps: 30, + canvasSize: { width: 1920, height: 1080 }, + canvasMode: "preset", +}; interface ProjectStore { activeProject: TProject | null; @@ -28,6 +44,7 @@ interface ProjectStore { options?: { backgroundColor?: string; blurIntensity?: number } ) => Promise; updateProjectFps: (fps: number) => Promise; + updateCanvasSize: (size: CanvasSize, mode: CanvasMode) => Promise; // Bookmark methods toggleBookmark: (time: number) => Promise; @@ -144,18 +161,7 @@ export const useProjectStore = create((set, get) => ({ }, createNewProject: async (name: string) => { - const newProject: TProject = { - id: generateUUID(), - name, - thumbnail: "", - createdAt: new Date(), - updatedAt: new Date(), - backgroundColor: "#000000", - backgroundType: "color", - blurIntensity: 8, - bookmarks: [], - fps: 30, - }; + const newProject: TProject = { ...DEFAULT_PROJECT, name }; set({ activeProject: newProject }); @@ -428,6 +434,29 @@ export const useProjectStore = create((set, get) => ({ } }, + updateCanvasSize: async (size: CanvasSize, mode: CanvasMode) => { + const { activeProject } = get(); + if (!activeProject) return; + + const updatedProject = { + ...activeProject, + canvasSize: size, + canvasMode: mode, + updatedAt: new Date(), + }; + + try { + await storageService.saveProject(updatedProject); + set({ activeProject: updatedProject }); + await get().loadAllProjects(); // Refresh the list + } catch (error) { + console.error("Failed to update canvas size:", error); + toast.error("Failed to update canvas size", { + description: "Please try again", + }); + } + }, + getFilteredAndSortedProjects: (searchQuery: string, sortOption: string) => { const { savedProjects } = get(); diff --git a/apps/web/src/stores/timeline-store.ts b/apps/web/src/stores/timeline-store.ts index 71ddedca..ed2ed612 100644 --- a/apps/web/src/stores/timeline-store.ts +++ b/apps/web/src/stores/timeline-store.ts @@ -10,17 +10,16 @@ import { ensureMainTrack, validateElementTrackCompatibility, } from "@/types/timeline"; -import { useEditorStore } from "./editor-store"; import { useMediaStore, getMediaAspectRatio, type MediaItem, } from "./media-store"; +import { findBestCanvasPreset } from "@/lib/editor-utils"; import { storageService } from "@/lib/storage/storage-service"; import { useProjectStore } from "./project-store"; import { generateUUID } from "@/lib/utils"; import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants"; -import { toast } from "sonner"; import { checkElementOverlaps, resolveElementOverlaps } from "@/lib/timeline"; // Helper function to manage element naming with suffixes @@ -533,9 +532,10 @@ export const useTimelineStore = create((set, get) => { mediaItem && (mediaItem.type === "image" || mediaItem.type === "video") ) { - const editorStore = useEditorStore.getState(); - editorStore.setCanvasSizeFromAspectRatio( - getMediaAspectRatio(mediaItem) + const projectStore = useProjectStore.getState(); + projectStore.updateCanvasSize( + findBestCanvasPreset(getMediaAspectRatio(mediaItem)), + "original" ); } diff --git a/apps/web/src/types/editor.ts b/apps/web/src/types/editor.ts index 12b474fd..6cbc6a6a 100644 --- a/apps/web/src/types/editor.ts +++ b/apps/web/src/types/editor.ts @@ -1,5 +1,7 @@ export type BackgroundType = "blur" | "mirror" | "color"; +export type CanvasMode = "preset" | "original" | "custom"; + export interface CanvasSize { width: number; height: number; diff --git a/apps/web/src/types/project.ts b/apps/web/src/types/project.ts index f52cba73..bf510942 100644 --- a/apps/web/src/types/project.ts +++ b/apps/web/src/types/project.ts @@ -1,3 +1,5 @@ +import { CanvasSize } from "./editor"; + export interface TProject { id: string; name: string; @@ -10,4 +12,12 @@ export interface TProject { blurIntensity?: number; // in pixels (4, 8, 18) fps?: number; bookmarks?: number[]; + canvasSize: CanvasSize; + /** + * Tracks how the canvas size was set: + * - "preset": User selected a standard preset (e.g. 16:9, 9:16) + * - "original": Using the first media item's dimensions + * - "custom": User set a custom aspect ratio or dimensions + */ + canvasMode: "preset" | "original" | "custom"; } -- cgit v1.3.1 From 1bed89d55f8301f94b69bc0859e85a28a0fe16af Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Tue, 5 Aug 2025 06:28:45 +0200 Subject: fix: typescript error from old usage --- apps/web/src/components/editor/preview-panel.tsx | 28 +++++++++++------------- apps/web/src/stores/project-store.ts | 4 +++- 2 files changed, 16 insertions(+), 16 deletions(-) (limited to 'apps/web') diff --git a/apps/web/src/components/editor/preview-panel.tsx b/apps/web/src/components/editor/preview-panel.tsx index f4cd2016..d965d25f 100644 --- a/apps/web/src/components/editor/preview-panel.tsx +++ b/apps/web/src/components/editor/preview-panel.tsx @@ -8,20 +8,13 @@ import { useEditorStore } from "@/stores/editor-store"; import { VideoPlayer } from "@/components/ui/video-player"; import { AudioPlayer } from "@/components/ui/audio-player"; import { Button } from "@/components/ui/button"; -import { - Play, - Pause, - Expand, - SkipBack, - SkipForward, - LayoutGrid, -} from "lucide-react"; +import { Play, Pause, Expand, SkipBack, SkipForward } from "lucide-react"; import { useState, useRef, useEffect, useCallback } from "react"; import { cn } from "@/lib/utils"; import { formatTimeCode } from "@/lib/time"; import { EditableTimecode } from "@/components/ui/editable-timecode"; import { FONT_CLASS_MAP } from "@/lib/font-config"; -import { useProjectStore } from "@/stores/project-store"; +import { DEFAULT_CANVAS_SIZE, useProjectStore } from "@/stores/project-store"; import { TextElementDragState } from "@/types/editor"; import { Popover, @@ -43,8 +36,8 @@ interface ActiveElement { export function PreviewPanel() { const { tracks, getTotalDuration, updateTextElement } = useTimelineStore(); const { mediaItems } = useMediaStore(); - const { currentTime, toggle, setCurrentTime, isPlaying } = usePlaybackStore(); - const { canvasSize } = useEditorStore(); + const { currentTime, toggle, setCurrentTime } = usePlaybackStore(); + const { activeProject } = useProjectStore(); const previewRef = useRef(null); const containerRef = useRef(null); const [previewDimensions, setPreviewDimensions] = useState({ @@ -52,7 +45,8 @@ export function PreviewPanel() { height: 0, }); const [isExpanded, setIsExpanded] = useState(false); - const { activeProject } = useProjectStore(); + + const canvasSize = activeProject?.canvasSize || DEFAULT_CANVAS_SIZE; const [dragState, setDragState] = useState({ isDragging: false, elementId: null, @@ -867,7 +861,9 @@ function PreviewToolbar({ toggleLayoutGuide(layoutGuide.platform || "tiktok")} + onCheckedChange={() => + toggleLayoutGuide(layoutGuide.platform || "tiktok") + } />
@@ -875,8 +871,10 @@ function PreviewToolbar({
toggleLayoutGuide(platform as PlatformLayout)} + checked={layoutGuide.platform === platform} + onCheckedChange={() => + toggleLayoutGuide(platform as PlatformLayout) + } />
diff --git a/apps/web/src/stores/project-store.ts b/apps/web/src/stores/project-store.ts index db6d014a..259533bf 100644 --- a/apps/web/src/stores/project-store.ts +++ b/apps/web/src/stores/project-store.ts @@ -7,6 +7,8 @@ import { useTimelineStore } from "./timeline-store"; import { generateUUID } from "@/lib/utils"; import { CanvasSize, CanvasMode } from "@/types/editor"; +export const DEFAULT_CANVAS_SIZE: CanvasSize = { width: 1920, height: 1080 }; + const DEFAULT_PROJECT: TProject = { id: generateUUID(), name: "Untitled", @@ -18,7 +20,7 @@ const DEFAULT_PROJECT: TProject = { blurIntensity: 8, bookmarks: [], fps: 30, - canvasSize: { width: 1920, height: 1080 }, + canvasSize: DEFAULT_CANVAS_SIZE, canvasMode: "preset", }; -- cgit v1.3.1 From 8bbb41a3cb7679d570fde3032e5bdc72b23d029b Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Tue, 5 Aug 2025 06:41:59 +0200 Subject: fix: ts error causing audio waveform signal issue --- apps/web/src/hooks/use-aspect-ratio.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) (limited to 'apps/web') diff --git a/apps/web/src/hooks/use-aspect-ratio.ts b/apps/web/src/hooks/use-aspect-ratio.ts index e3e85141..4bc7cfd5 100644 --- a/apps/web/src/hooks/use-aspect-ratio.ts +++ b/apps/web/src/hooks/use-aspect-ratio.ts @@ -1,12 +1,17 @@ import { useEditorStore } from "@/stores/editor-store"; import { useMediaStore, getMediaAspectRatio } from "@/stores/media-store"; import { useTimelineStore } from "@/stores/timeline-store"; +import { useProjectStore } from "@/stores/project-store"; export function useAspectRatio() { - const { canvasSize, canvasMode, canvasPresets } = useEditorStore(); + const { canvasPresets } = useEditorStore(); + const { activeProject } = useProjectStore(); const { mediaItems } = useMediaStore(); const { tracks } = useTimelineStore(); + const canvasSize = activeProject?.canvasSize || { width: 1920, height: 1080 }; + const canvasMode = activeProject?.canvasMode || "preset"; + // Find the current preset based on canvas size const currentPreset = canvasPresets.find( (preset) => -- cgit v1.3.1 From 65b04f5759b04ad29d299a12fe7ee615daef7e70 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Tue, 5 Aug 2025 07:11:03 +0200 Subject: ts error --- apps/web/src/hooks/use-aspect-ratio.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'apps/web') diff --git a/apps/web/src/hooks/use-aspect-ratio.ts b/apps/web/src/hooks/use-aspect-ratio.ts index e3e85141..35d2cec4 100644 --- a/apps/web/src/hooks/use-aspect-ratio.ts +++ b/apps/web/src/hooks/use-aspect-ratio.ts @@ -1,9 +1,13 @@ import { useEditorStore } from "@/stores/editor-store"; import { useMediaStore, getMediaAspectRatio } from "@/stores/media-store"; import { useTimelineStore } from "@/stores/timeline-store"; +import { DEFAULT_CANVAS_SIZE, useProjectStore } from "@/stores/project-store"; export function useAspectRatio() { - const { canvasSize, canvasMode, canvasPresets } = useEditorStore(); + const { canvasPresets } = useEditorStore(); + const { activeProject } = useProjectStore(); + const canvasSize = activeProject?.canvasSize || DEFAULT_CANVAS_SIZE; + const canvasMode = activeProject?.canvasMode || "preset"; const { mediaItems } = useMediaStore(); const { tracks } = useTimelineStore(); -- cgit v1.3.1 From dee434cd33144950905eb349726d7bbc3c00c624 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Tue, 5 Aug 2025 07:14:24 +0200 Subject: style: padding top in preview toolbar --- apps/web/src/components/editor/preview-panel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'apps/web') diff --git a/apps/web/src/components/editor/preview-panel.tsx b/apps/web/src/components/editor/preview-panel.tsx index d965d25f..b92922d4 100644 --- a/apps/web/src/components/editor/preview-panel.tsx +++ b/apps/web/src/components/editor/preview-panel.tsx @@ -832,7 +832,7 @@ function PreviewToolbar({ return (
-- cgit v1.3.1 From e4546120008b9e5cd97ab1f8a0c63c55559dae1d Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Tue, 5 Aug 2025 07:25:14 +0200 Subject: formatting --- apps/web/src/components/editor/audio-waveform.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) (limited to 'apps/web') diff --git a/apps/web/src/components/editor/audio-waveform.tsx b/apps/web/src/components/editor/audio-waveform.tsx index 372a31c5..79bf401d 100644 --- a/apps/web/src/components/editor/audio-waveform.tsx +++ b/apps/web/src/components/editor/audio-waveform.tsx @@ -20,7 +20,7 @@ const AudioWaveform: React.FC = ({ useEffect(() => { let mounted = true; let ws = wavesurfer.current; - + const initWaveSurfer = async () => { if (!waveformRef.current || !audioUrl) return; @@ -90,7 +90,7 @@ const AudioWaveform: React.FC = ({ const wsToDestroy = ws; // Detach from ref immediately wavesurfer.current = null; - + // Wait a tick to destroy so any pending operations can complete requestAnimationFrame(() => { try { @@ -111,13 +111,13 @@ const AudioWaveform: React.FC = ({ return () => { // Mark component as unmounted mounted = false; - + // Store reference to current wavesurfer instance const wsToDestroy = wavesurfer.current; - + // Immediately clear the ref to prevent accessing it after unmount wavesurfer.current = null; - + // If we have an instance to clean up, do it safely if (wsToDestroy) { // Delay destruction to avoid race conditions -- cgit v1.3.1 From 949228f0bb0c3776b0459b9bbd93af4b18094626 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Tue, 5 Aug 2025 08:26:39 +0200 Subject: refactor: remove unused imports --- .../src/components/editor/media-panel/tabbar.tsx | 26 ++++++++++++++-------- 1 file changed, 17 insertions(+), 9 deletions(-) (limited to 'apps/web') diff --git a/apps/web/src/components/editor/media-panel/tabbar.tsx b/apps/web/src/components/editor/media-panel/tabbar.tsx index 4c3965c4..56437508 100644 --- a/apps/web/src/components/editor/media-panel/tabbar.tsx +++ b/apps/web/src/components/editor/media-panel/tabbar.tsx @@ -2,12 +2,11 @@ import { cn } from "@/lib/utils"; import { Tab, tabs, useMediaPanelStore } from "./store"; -import { Button } from "@/components/ui/button"; -import { ChevronRight, ChevronLeft } from "lucide-react"; -import { useRef, useState, useEffect } from "react"; -import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; - - +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; export function TabBar() { const { activeTab, setActiveTab } = useMediaPanelStore(); @@ -21,7 +20,9 @@ export function TabBar() {
setActiveTab(tabKey)} key={tabKey} @@ -30,8 +31,15 @@ export function TabBar() { - -
{tab.label}
+ +
+ {tab.label} +
-- cgit v1.3.1 From efbbc48bc5d20b8df3c1fe5feab3f6471263da4e Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Tue, 5 Aug 2025 08:46:17 +0200 Subject: refactor: create base view for media panel tabs --- .../src/components/editor/media-panel/index.tsx | 7 +-- .../editor/media-panel/views/base-view.tsx | 59 ++++++++++++++++++++++ .../editor/media-panel/views/captions.tsx | 9 ++++ .../editor/media-panel/views/settings.tsx | 38 ++++++-------- .../components/editor/media-panel/views/text.tsx | 5 +- 5 files changed, 89 insertions(+), 29 deletions(-) create mode 100644 apps/web/src/components/editor/media-panel/views/base-view.tsx create mode 100644 apps/web/src/components/editor/media-panel/views/captions.tsx (limited to 'apps/web') diff --git a/apps/web/src/components/editor/media-panel/index.tsx b/apps/web/src/components/editor/media-panel/index.tsx index 70eb69dd..21df9924 100644 --- a/apps/web/src/components/editor/media-panel/index.tsx +++ b/apps/web/src/components/editor/media-panel/index.tsx @@ -7,6 +7,7 @@ import { TextView } from "./views/text"; import { SoundsView } from "./views/sounds"; import { Separator } from "@/components/ui/separator"; import { SettingsView } from "./views/settings"; +import { Captions } from "./views/captions"; export function MediaPanel() { const { activeTab } = useMediaPanelStore(); @@ -30,11 +31,7 @@ export function MediaPanel() { Transitions view coming soon...
), - captions: ( -
- Captions view coming soon... -
- ), + captions: , filters: (
Filters view coming soon... diff --git a/apps/web/src/components/editor/media-panel/views/base-view.tsx b/apps/web/src/components/editor/media-panel/views/base-view.tsx new file mode 100644 index 00000000..41e9a2b6 --- /dev/null +++ b/apps/web/src/components/editor/media-panel/views/base-view.tsx @@ -0,0 +1,59 @@ +import { ScrollArea } from "@/components/ui/scroll-area"; +import { Separator } from "@/components/ui/separator"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; + +interface BaseViewProps { + children: React.ReactNode; + defaultTab?: string; + tabs?: { + value: string; + label: string; + content: React.ReactNode; + }[]; + className?: string; +} + +function ViewContent({ children }: { children: React.ReactNode }) { + return ( + +
{children}
+
+ ); +} + +export function BaseView({ + children, + defaultTab, + tabs, + className = "", +}: BaseViewProps) { + return ( +
+ {!tabs || tabs.length === 0 ? ( + {children} + ) : ( + +
+ + {tabs.map((tab) => ( + + {tab.label} + + ))} + +
+ + {tabs.map((tab) => ( + + {tab.content} + + ))} +
+ )} +
+ ); +} diff --git a/apps/web/src/components/editor/media-panel/views/captions.tsx b/apps/web/src/components/editor/media-panel/views/captions.tsx new file mode 100644 index 00000000..ef919fec --- /dev/null +++ b/apps/web/src/components/editor/media-panel/views/captions.tsx @@ -0,0 +1,9 @@ +import { BaseView } from "./base-view"; + +export function Captions() { + return ( + +
Captions
+
+ ); +} diff --git a/apps/web/src/components/editor/media-panel/views/settings.tsx b/apps/web/src/components/editor/media-panel/views/settings.tsx index b6b79b59..419cd1ee 100644 --- a/apps/web/src/components/editor/media-panel/views/settings.tsx +++ b/apps/web/src/components/editor/media-panel/views/settings.tsx @@ -7,9 +7,7 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; -import { Separator } from "@/components/ui/separator"; -import { ScrollArea } from "@/components/ui/scroll-area"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { BaseView } from "./base-view"; import { PropertyItem, PropertyItemLabel, @@ -34,25 +32,21 @@ export function SettingsView() { function ProjectSettingsTabs() { return ( -
- -
- - Project info - Background - -
- - - - - - - - - -
-
+ , + }, + { + value: "background", + label: "Background", + content: , + }, + ]} + /> ); } diff --git a/apps/web/src/components/editor/media-panel/views/text.tsx b/apps/web/src/components/editor/media-panel/views/text.tsx index ca8e88c9..49eacbc7 100644 --- a/apps/web/src/components/editor/media-panel/views/text.tsx +++ b/apps/web/src/components/editor/media-panel/views/text.tsx @@ -1,4 +1,5 @@ import { DraggableMediaItem } from "@/components/ui/draggable-item"; +import { BaseView } from "./base-view"; import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants"; import { useTimelineStore } from "@/stores/timeline-store"; import { type TextElement } from "@/types/timeline"; @@ -28,7 +29,7 @@ const textData: TextElement = { export function TextView() { return ( -
+ -
+
); } -- cgit v1.3.1 From 539cd45f642f137dd297ed00305e1782bbd679e2 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Tue, 5 Aug 2025 08:49:17 +0200 Subject: style: input component bg color --- apps/web/src/components/rename-project-dialog.tsx | 2 +- apps/web/src/components/ui/input.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'apps/web') diff --git a/apps/web/src/components/rename-project-dialog.tsx b/apps/web/src/components/rename-project-dialog.tsx index e2b01be0..c50f9f71 100644 --- a/apps/web/src/components/rename-project-dialog.tsx +++ b/apps/web/src/components/rename-project-dialog.tsx @@ -51,7 +51,7 @@ export function RenameProjectDialog({ } }} placeholder="Enter a new name" - className="mt-4 bg-background/50" + className="mt-4" /> diff --git a/apps/web/src/components/ui/input.tsx b/apps/web/src/components/ui/input.tsx index 440191f2..65c7660d 100644 --- a/apps/web/src/components/ui/input.tsx +++ b/apps/web/src/components/ui/input.tsx @@ -51,7 +51,7 @@ const Input = React.forwardRef( Date: Tue, 5 Aug 2025 08:51:27 +0200 Subject: style: spacing --- apps/web/src/components/editor/media-panel/views/settings.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'apps/web') diff --git a/apps/web/src/components/editor/media-panel/views/settings.tsx b/apps/web/src/components/editor/media-panel/views/settings.tsx index 419cd1ee..d33b4cce 100644 --- a/apps/web/src/components/editor/media-panel/views/settings.tsx +++ b/apps/web/src/components/editor/media-panel/views/settings.tsx @@ -257,7 +257,7 @@ function BackgroundView() { ); return ( -
+
{blurPreviews}
-- cgit v1.3.1 From 2a7cc13d3eee7fdad3c1174d17eee8a189f12afc Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Tue, 5 Aug 2025 09:06:13 +0200 Subject: fix: ts error --- apps/web/src/components/editor/media-panel/views/base-view.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'apps/web') diff --git a/apps/web/src/components/editor/media-panel/views/base-view.tsx b/apps/web/src/components/editor/media-panel/views/base-view.tsx index 41e9a2b6..296295fc 100644 --- a/apps/web/src/components/editor/media-panel/views/base-view.tsx +++ b/apps/web/src/components/editor/media-panel/views/base-view.tsx @@ -3,7 +3,7 @@ import { Separator } from "@/components/ui/separator"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; interface BaseViewProps { - children: React.ReactNode; + children?: React.ReactNode; defaultTab?: string; tabs?: { value: string; -- cgit v1.3.1 From 9c6556ea05693701450c1977267b761e0fb3488c Mon Sep 17 00:00:00 2001 From: Mathis Boulais <77585805+MathisBls@users.noreply.github.com> Date: Tue, 5 Aug 2025 12:57:47 +0200 Subject: fix: restore horizontal scroll sync after Radix ScrollArea removal (#518) --- apps/web/src/components/editor/timeline/index.tsx | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) (limited to 'apps/web') diff --git a/apps/web/src/components/editor/timeline/index.tsx b/apps/web/src/components/editor/timeline/index.tsx index dd81b7b3..bb8dfbc0 100644 --- a/apps/web/src/components/editor/timeline/index.tsx +++ b/apps/web/src/components/editor/timeline/index.tsx @@ -513,15 +513,9 @@ export function Timeline() { // --- Scroll synchronization effect --- useEffect(() => { - const rulerViewport = rulerScrollRef.current?.querySelector( - "[data-radix-scroll-area-viewport]" - ) as HTMLElement; - const tracksViewport = tracksScrollRef.current?.querySelector( - "[data-radix-scroll-area-viewport]" - ) as HTMLElement; - const trackLabelsViewport = trackLabelsScrollRef.current?.querySelector( - "[data-radix-scroll-area-viewport]" - ) as HTMLElement; + const rulerViewport = rulerScrollRef.current; + const tracksViewport = tracksScrollRef.current; + const trackLabelsViewport = trackLabelsScrollRef.current; if (!rulerViewport || !tracksViewport) return; -- cgit v1.3.1 From 4f6d0142866653507fabf34644a238be44951b30 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Tue, 5 Aug 2025 14:07:05 +0200 Subject: fix: remaining timeline issues with targeting wrong radix ui scroll element --- apps/web/src/components/editor/snap-indicator.tsx | 4 +--- .../src/components/editor/timeline/timeline-playhead.tsx | 8 ++------ apps/web/src/hooks/use-timeline-playhead.ts | 16 ++++------------ 3 files changed, 7 insertions(+), 21 deletions(-) (limited to 'apps/web') diff --git a/apps/web/src/components/editor/snap-indicator.tsx b/apps/web/src/components/editor/snap-indicator.tsx index 0a51e107..ac884b71 100644 --- a/apps/web/src/components/editor/snap-indicator.tsx +++ b/apps/web/src/components/editor/snap-indicator.tsx @@ -28,9 +28,7 @@ export function SnapIndicator({ // Track scroll position to lock snap indicator to frame useEffect(() => { - const tracksViewport = tracksScrollRef.current?.querySelector( - "[data-radix-scroll-area-viewport]" - ) as HTMLElement; + const tracksViewport = tracksScrollRef.current; if (!tracksViewport) return; diff --git a/apps/web/src/components/editor/timeline/timeline-playhead.tsx b/apps/web/src/components/editor/timeline/timeline-playhead.tsx index 695843a4..e84512ce 100644 --- a/apps/web/src/components/editor/timeline/timeline-playhead.tsx +++ b/apps/web/src/components/editor/timeline/timeline-playhead.tsx @@ -51,9 +51,7 @@ export function TimelinePlayhead({ // Track scroll position to lock playhead to frame useEffect(() => { - const tracksViewport = tracksScrollRef.current?.querySelector( - "[data-radix-scroll-area-viewport]" - ) as HTMLElement; + const tracksViewport = tracksScrollRef.current; if (!tracksViewport) return; @@ -86,9 +84,7 @@ export function TimelinePlayhead({ // Get the timeline content width and viewport width for right boundary const timelineContentWidth = duration * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel; - const tracksViewport = tracksScrollRef.current?.querySelector( - "[data-radix-scroll-area-viewport]" - ) as HTMLElement; + const tracksViewport = tracksScrollRef.current; const viewportWidth = tracksViewport?.clientWidth || 1000; // Constrain playhead to never appear outside the timeline area diff --git a/apps/web/src/hooks/use-timeline-playhead.ts b/apps/web/src/hooks/use-timeline-playhead.ts index ac59a8cd..08587228 100644 --- a/apps/web/src/hooks/use-timeline-playhead.ts +++ b/apps/web/src/hooks/use-timeline-playhead.ts @@ -119,12 +119,8 @@ export function useTimelinePlayhead({ // Auto-scroll function during dragging const performAutoScroll = useCallback(() => { - const rulerViewport = rulerScrollRef.current?.querySelector( - "[data-radix-scroll-area-viewport]" - ) as HTMLElement; - const tracksViewport = tracksScrollRef.current?.querySelector( - "[data-radix-scroll-area-viewport]" - ) as HTMLElement; + const rulerViewport = rulerScrollRef.current; + const tracksViewport = tracksScrollRef.current; if (!rulerViewport || !tracksViewport || !isScrubbing) return; @@ -240,12 +236,8 @@ export function useTimelinePlayhead({ // Only auto-scroll during playback, not during manual interactions if (!isPlaying || isScrubbing) return; - const rulerViewport = rulerScrollRef.current?.querySelector( - "[data-radix-scroll-area-viewport]" - ) as HTMLElement; - const tracksViewport = tracksScrollRef.current?.querySelector( - "[data-radix-scroll-area-viewport]" - ) as HTMLElement; + const rulerViewport = rulerScrollRef.current; + const tracksViewport = tracksScrollRef.current; if (!rulerViewport || !tracksViewport) return; const playheadPx = playheadPosition * 50 * zoomLevel; // TIMELINE_CONSTANTS.PIXELS_PER_SECOND = 50 -- cgit v1.3.1 From f8724851fd547b212fcb5f357c911564f7b8240b Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Tue, 5 Aug 2025 15:03:38 +0200 Subject: style: re-design export button (design credit: @AdamBartas on twitter) --- apps/web/src/components/editor-header.tsx | 41 ++++++++++++++++++++----------- apps/web/src/components/icons.tsx | 38 ++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 15 deletions(-) (limited to 'apps/web') diff --git a/apps/web/src/components/editor-header.tsx b/apps/web/src/components/editor-header.tsx index d2333417..3a7ee3a0 100644 --- a/apps/web/src/components/editor-header.tsx +++ b/apps/web/src/components/editor-header.tsx @@ -29,6 +29,7 @@ import { useRouter } from "next/navigation"; import { FaDiscord } from "react-icons/fa6"; import { useTheme } from "next-themes"; import { usePlaybackStore } from "@/stores/playback-store"; +import { TransitionUpIcon } from "./icons"; export function EditorHeader() { const { getTotalDuration } = useTimelineStore(); @@ -39,13 +40,6 @@ export function EditorHeader() { const router = useRouter(); const { theme, setTheme } = useTheme(); - const handleExport = () => { - // TODO: Implement export functionality - // NOTE: This is already being worked on - console.log("Export project"); - window.open("https://youtube.com/watch?v=dQw4w9WgXcQ", "_blank"); - }; - const handleNameSave = async (newName: string) => { console.log("handleNameSave", newName); if (activeProject && newName.trim() && newName !== activeProject.name) { @@ -148,14 +142,7 @@ export function EditorHeader() { const rightContent = (
-- cgit v1.3.1 From e724a086841c2212c7249b25fd4626d870f57ced Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Tue, 5 Aug 2025 15:20:42 +0200 Subject: style: complete export button ui --- apps/web/src/components/editor-header.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'apps/web') diff --git a/apps/web/src/components/editor-header.tsx b/apps/web/src/components/editor-header.tsx index f68d145b..e6e26ce9 100644 --- a/apps/web/src/components/editor-header.tsx +++ b/apps/web/src/components/editor-header.tsx @@ -178,11 +178,11 @@ function ExportButton() { className="flex items-center gap-1.5 bg-[#38BDF8] text-white rounded-md px-[0.1rem] py-[0.1rem] cursor-pointer hover:brightness-95 transition-all duration-200" onClick={handleExport} > -
+
Export -
-
+
+
-- cgit v1.3.1 From 6aaa9a1f585ac4345df507b487d95bdf21769adb Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Tue, 5 Aug 2025 15:21:26 +0200 Subject: nvm one more thing --- apps/web/src/components/editor-header.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'apps/web') diff --git a/apps/web/src/components/editor-header.tsx b/apps/web/src/components/editor-header.tsx index e6e26ce9..0caa6c3e 100644 --- a/apps/web/src/components/editor-header.tsx +++ b/apps/web/src/components/editor-header.tsx @@ -181,7 +181,7 @@ function ExportButton() {
Export -
+
-- cgit v1.3.1 From e5a0deb777be91eef119be45ca9f5a95aadad0c6 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Tue, 5 Aug 2025 17:18:41 +0200 Subject: style: move icons on timeline toolbar --- apps/web/src/components/editor/preview-panel.tsx | 28 ++++++++++++------------ 1 file changed, 14 insertions(+), 14 deletions(-) (limited to 'apps/web') diff --git a/apps/web/src/components/editor/preview-panel.tsx b/apps/web/src/components/editor/preview-panel.tsx index b92922d4..cc58a598 100644 --- a/apps/web/src/components/editor/preview-panel.tsx +++ b/apps/web/src/components/editor/preview-panel.tsx @@ -835,12 +835,25 @@ function PreviewToolbar({ className="flex justify-end gap-2 h-auto pb-5 pr-5 pt-4 w-full" >
+
-
) : null} @@ -771,13 +764,6 @@ function FullscreenPreview({ ) )} - {activeProject?.backgroundType === "blur" && - blurBackgroundElements.length === 0 && - activeElements.length > 0 && ( -
- Add a video or image to use blur background -
- )}
-- cgit v1.3.1 From 54e990af26adb6e4ce467a0e9e511e52acc865e1 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Sat, 9 Aug 2025 11:02:01 +0200 Subject: Merge pull request [#514](https://github.com/mazeincoding/AppCut/issues/514) --- apps/web/src/app/editor/[project_id]/page.tsx | 344 ++++++++++++++++++---- apps/web/src/components/editor-header.tsx | 2 + apps/web/src/components/panel-preset-selector.tsx | 91 ++++++ apps/web/src/stores/panel-store.ts | 214 ++++++++++++-- 4 files changed, 575 insertions(+), 76 deletions(-) create mode 100644 apps/web/src/components/panel-preset-selector.tsx (limited to 'apps/web') 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() {
- - - {/* Main content area */} - - {/* Tools Panel */} - + + + + + + - - + + + + + + + - + + + + + - {/* Preview Area */} - + + + + + + + + ) : activePreset === "inspector" ? ( + + setPropertiesPanel(100 - size)} + className="min-w-0 min-h-0" + > + - - + + + + + + + - + + + + + - + + + + + + + + + + + + + + ) : activePreset === "vertical-preview" ? ( + + setPreviewPanel(100 - size)} + className="min-w-0 min-h-0" + > + - - - - - - - - {/* Timeline */} - + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) : ( + - - - + + {/* Main content area */} + + {/* Tools Panel */} + + + + + + + {/* Preview Area */} + + + + + + + + + + + + + + + {/* Timeline */} + + + + + )}
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 = (