From 6e4069b42fbd0f97d063e967bf4205cbec033778 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Tue, 20 Jan 2026 22:14:49 +0100 Subject: [PATCH] shipping this slop --- .cursor/rules/codebase-index.mdc | 74 +- .vscode/settings.json | 4 +- apps/web/src/app/editor/[project_id]/page.tsx | 273 +----- .../editor/assets-panel/views/captions.tsx | 7 +- .../editor/assets-panel/views/media.tsx | 164 ++-- .../editor/assets-panel/views/settings.tsx | 30 +- .../editor/assets-panel/views/stickers.tsx | 22 +- .../editor/assets-panel/views/text.tsx | 12 +- .../src/components/editor/editor-header.tsx | 4 +- .../src/components/editor/export-button.tsx | 24 +- .../editor/panel-preset-selector.tsx | 91 -- .../properties-panel/text-properties.tsx | 4 +- .../components/editor/timeline/bookmarks.tsx | 3 +- .../src/components/editor/timeline/index.tsx | 57 +- .../editor/timeline/timeline-element.tsx | 70 +- .../editor/timeline/timeline-playhead.tsx | 2 +- .../editor/timeline/timeline-ruler.tsx | 5 +- .../editor/timeline/timeline-toolbar.tsx | 10 +- .../editor/timeline/timeline-track.tsx | 14 +- .../components/keyboard-shortcuts-help.tsx | 16 +- apps/web/src/components/storage-provider.tsx | 2 +- apps/web/src/components/ui/button.tsx | 2 +- apps/web/src/components/ui/dialog.tsx | 2 +- apps/web/src/components/ui/draggable-item.tsx | 3 +- .../web/src/components/ui/navigation-menu.tsx | 2 +- apps/web/src/constants/editor-constants.ts | 10 + apps/web/src/constants/timeline-constants.tsx | 17 +- apps/web/src/core/index.ts | 63 -- apps/web/src/core/managers/media-manager.ts | 8 +- apps/web/src/core/managers/project-manager.ts | 19 +- apps/web/src/core/managers/save-manager.ts | 2 +- apps/web/src/core/managers/scenes-manager.ts | 6 +- .../web/src/core/managers/timeline-manager.ts | 16 +- apps/web/src/data/colors/pattern-craft.ts | 48 +- .../src/hooks/actions/use-editor-actions.ts | 27 +- .../element/use-element-interaction.ts | 94 +- .../timeline/element/use-element-resize.ts | 63 +- .../src/hooks/timeline/use-selection-box.ts | 4 +- .../hooks/timeline/use-timeline-drag-drop.ts | 121 ++- .../timeline/use-timeline-interactions.ts | 24 +- .../src/hooks/timeline/use-timeline-zoom.ts | 10 +- apps/web/src/lib/audio-utils.ts | 92 +- apps/web/src/lib/canvas/gradients.ts | 264 ----- .../src/lib/commands/media/add-media-asset.ts | 2 +- .../lib/commands/media/remove-media-asset.ts | 6 +- .../project/update-project-settings.ts | 4 +- .../src/lib/commands/scene/remove-bookmark.ts | 2 +- .../src/lib/commands/scene/toggle-bookmark.ts | 2 +- .../lib/commands/timeline/clipboard/paste.ts | 219 ++++- .../commands/timeline/element/add-element.ts | 131 --- .../timeline/element/duplicate-elements.ts | 69 +- .../lib/commands/timeline/element/index.ts | 4 +- .../timeline/element/insert-element.ts | 312 ++++++ .../{move-element.ts => move-elements.ts} | 0 .../timeline/element/update-element-trim.ts | 20 +- .../lib/commands/timeline/track/add-track.ts | 20 +- apps/web/src/lib/drag-data.ts | 22 +- apps/web/src/lib/export-utils.ts | 118 --- apps/web/src/lib/export.ts | 15 + apps/web/src/lib/gradients/canvas.ts | 906 ++++++++++++++++++ apps/web/src/lib/gradients/index.ts | 2 + apps/web/src/lib/gradients/parser.ts | 641 +++++++++++++ .../mediabunny.ts} | 0 .../processing.ts} | 4 +- .../{bookmark-utils.ts => bookmarks.ts} | 0 apps/web/src/lib/timeline/drop-utils.ts | 45 +- apps/web/src/lib/timeline/element-utils.ts | 18 +- apps/web/src/lib/timeline/index.ts | 1 + apps/web/src/lib/timeline/track-utils.ts | 44 +- apps/web/src/lib/timeline/zoom-utils.ts | 22 + apps/web/src/lib/utils.ts | 5 + apps/web/src/lib/zk-encryption.ts | 71 -- .../{lib => services/media}/video-cache.ts | 76 +- .../renderer/nodes/blur-background-node.ts | 37 +- .../src/services/renderer/nodes/color-node.ts | 11 + .../src/services/renderer/scene-builder.ts | 146 +-- .../storage/indexeddb-adapter.ts | 0 .../storage}/migrations/base.ts | 0 .../storage}/migrations/index.ts | 0 .../storage}/migrations/runner.ts | 2 +- .../storage}/migrations/v0-to-v1.ts | 4 +- .../storage}/migrations/v1-to-v2.ts | 74 +- .../storage}/migrations/version-manager.ts | 2 +- .../{lib => services}/storage/opfs-adapter.ts | 0 .../storage/storage-service.ts | 22 +- .../src/{lib => services}/storage/types.ts | 0 apps/web/src/stores/assets-panel-store.ts | 8 + apps/web/src/stores/panel-store.ts | 278 ++---- apps/web/src/stores/sounds-store.ts | 7 +- apps/web/src/stores/stickers-store.ts | 5 +- apps/web/src/stores/text-properties-store.ts | 6 +- apps/web/src/stores/timeline-store.ts | 6 +- apps/web/src/types/assets.ts | 2 +- apps/web/src/types/project.ts | 13 +- apps/web/src/types/timeline.ts | 4 +- legacy/preview-panel.tsx | 2 +- legacy/timeline-renderer.ts | 20 +- 97 files changed, 3357 insertions(+), 1858 deletions(-) delete mode 100644 apps/web/src/components/editor/panel-preset-selector.tsx delete mode 100644 apps/web/src/lib/canvas/gradients.ts delete mode 100644 apps/web/src/lib/commands/timeline/element/add-element.ts create mode 100644 apps/web/src/lib/commands/timeline/element/insert-element.ts rename apps/web/src/lib/commands/timeline/element/{move-element.ts => move-elements.ts} (100%) delete mode 100644 apps/web/src/lib/export-utils.ts create mode 100644 apps/web/src/lib/export.ts create mode 100644 apps/web/src/lib/gradients/canvas.ts create mode 100644 apps/web/src/lib/gradients/index.ts create mode 100644 apps/web/src/lib/gradients/parser.ts rename apps/web/src/lib/{mediabunny-utils.ts => media/mediabunny.ts} (100%) rename apps/web/src/lib/{media-processing-utils.ts => media/processing.ts} (98%) rename apps/web/src/lib/timeline/{bookmark-utils.ts => bookmarks.ts} (100%) create mode 100644 apps/web/src/lib/timeline/zoom-utils.ts delete mode 100644 apps/web/src/lib/zk-encryption.ts rename apps/web/src/{lib => services/media}/video-cache.ts (73%) rename apps/web/src/{lib => services}/storage/indexeddb-adapter.ts (100%) rename apps/web/src/{lib => services/storage}/migrations/base.ts (100%) rename apps/web/src/{lib => services/storage}/migrations/index.ts (100%) rename apps/web/src/{lib => services/storage}/migrations/runner.ts (97%) rename apps/web/src/{lib => services/storage}/migrations/v0-to-v1.ts (94%) rename apps/web/src/{lib => services/storage}/migrations/v1-to-v2.ts (80%) rename apps/web/src/{lib => services/storage}/migrations/version-manager.ts (96%) rename apps/web/src/{lib => services}/storage/opfs-adapter.ts (100%) rename apps/web/src/{lib => services}/storage/storage-service.ts (95%) rename apps/web/src/{lib => services}/storage/types.ts (100%) diff --git a/.cursor/rules/codebase-index.mdc b/.cursor/rules/codebase-index.mdc index 02187b9e..30660979 100644 --- a/.cursor/rules/codebase-index.mdc +++ b/.cursor/rules/codebase-index.mdc @@ -23,6 +23,7 @@ captions-constants.ts editor-constants.ts export const PLATFORM_LAYOUTS: Record + export const PANEL_CONFIG export-constants.ts export const DEFAULT_EXPORT_OPTIONS: ExportOptions @@ -84,7 +85,7 @@ text-constants.ts timeline-constants.tsx export const TRACK_COLORS: Record< TrackType, - { solid: string; background: string; border: string } + { background: string; } > export const TRACK_HEIGHTS: Record export const TRACK_GAP @@ -292,6 +293,7 @@ use-element-interaction.ts timelineRef, tracksContainerRef, tracksScrollRef, + snappingEnabled, onSnapPointChange, }: UseElementInteractionProps) @@ -358,23 +360,24 @@ blog-query.ts export function processHtmlContent({ html }: { html: string }): Promise drag-data.ts - export function setDragData({ - dataTransfer, - dragData, - }: { - dataTransfer: DataTransfer; - dragData: TimelineDragData; + export function setDragData({ + dataTransfer, + dragData, + }: { + dataTransfer: DataTransfer; + dragData: TimelineDragData; }): void - export function getDragData({ - dataTransfer, - }: { - dataTransfer: DataTransfer; + export function getDragData({ + dataTransfer, + }: { + dataTransfer: DataTransfer; }): TimelineDragData | null - export function hasDragData({ - dataTransfer, - }: { - dataTransfer: DataTransfer; + export function hasDragData({ + dataTransfer, + }: { + dataTransfer: DataTransfer; }): boolean + export function clearDragData(): void editor-utils.ts export function dimensionToAspectRatio({ @@ -636,6 +639,7 @@ utils.ts export function cn(...inputs: ClassValue[]): string export function formatDate({ date }: { date: Date }): string export function capitalizeFirstLetter({ string }: { string: string }) + export function uppercase({ string }: { string: string }) export function generateUUID(): string export function isTypableDOMElement({ element, @@ -893,6 +897,7 @@ drop-utils.ts elementDuration, pixelsPerSecond, zoomLevel, + verticalDragDirection, startTimeOverride, excludeElementId, }: ComputeDropTargetParams): DropTarget @@ -965,7 +970,7 @@ element-utils.ts name: string; duration: number; startTime: number; - buffer: AudioBuffer; + buffer?: AudioBuffer; }): CreateUploadAudioElement export function buildLibraryAudioElement({ sourceUrl, @@ -978,7 +983,7 @@ element-utils.ts name: string; duration: number; startTime: number; - buffer: AudioBuffer; + buffer?: AudioBuffer; }): CreateLibraryAudioElement index.ts @@ -1019,6 +1024,13 @@ track-utils.ts type: TrackType; name?: string; }): TimelineTrack + export function getDefaultInsertIndexForTrack({ + tracks, + trackType, + }: { + tracks: TimelineTrack[]; + trackType: TrackType; + }): number export function isMainTrack(track: TimelineTrack) export function getMainTrack({ tracks, @@ -1126,11 +1138,14 @@ keybindings-store.ts export const useKeybindingsStore panel-store.ts - export type PanelPreset = | "default" - | "media" - | "inspector" - | "vertical-preview" - export const PRESET_CONFIGS: Record + export interface PanelSizes { + tools: number + preview: number + properties: number + mainContent: number + timeline: number + } + export type PanelId = keyof PanelSizes export const usePanelStore sounds-store.ts @@ -1140,7 +1155,7 @@ stickers-store.ts export const useStickersStore text-properties-store.ts - export type TextPropertiesTab = "transform" | "style" + export type TextPropertiesTab = "text" | "transform" export interface TextPropertiesTabMeta { value: TextPropertiesTab label: string @@ -1284,12 +1299,13 @@ keybinding.ts project.ts export type TBackground = | { - type: "color"; - color: string; - } + type: "color"; + color: string; + } | { - type: "blur"; - blurIntensity... + type: "blur"; + blurIntensity: number; + } export interface TCanvasSize { width: number height: number @@ -1304,6 +1320,7 @@ project.ts export interface TProjectSettings { fps: number canvasSize: TCanvasSize + originalCanvasSize?: TCanvasSize | null background: TBackground } export interface TProject { @@ -1494,6 +1511,7 @@ timeline.ts elementDuration: number pixelsPerSecond: number zoomLevel: number + verticalDragDirection?: "up" | "down" | null startTimeOverride?: number excludeElementId?: string } diff --git a/.vscode/settings.json b/.vscode/settings.json index 5912f6c4..8eff0c83 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,10 +8,10 @@ "editor.formatOnPaste": true, "emmet.showExpandedAbbreviation": "never", "[typescriptreact]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" + "editor.defaultFormatter": "vscode.typescript-language-features" }, "[typescript]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" + "editor.defaultFormatter": "vscode.typescript-language-features" } } \ No newline at end of file diff --git a/apps/web/src/app/editor/[project_id]/page.tsx b/apps/web/src/app/editor/[project_id]/page.tsx index 5ffae328..ac013fbe 100644 --- a/apps/web/src/app/editor/[project_id]/page.tsx +++ b/apps/web/src/app/editor/[project_id]/page.tsx @@ -11,10 +11,10 @@ import { PropertiesPanel } from "@/components/editor/properties-panel"; import { Timeline } from "@/components/editor/timeline"; import { PreviewPanel } from "@/components/editor/preview-panel"; import { EditorHeader } from "@/components/editor/editor-header"; -import { usePanelStore } from "@/stores/panel-store"; import { EditorProvider } from "@/components/providers/editor-provider"; import { Onboarding } from "@/components/editor/onboarding"; import { MigrationDialog } from "@/components/editor/migration-dialog"; +import { usePanelStore } from "@/stores/panel-store"; export default function Editor() { const params = useParams(); @@ -34,271 +34,37 @@ export default function Editor() { ); } -function EditorLayout({}: {}) { - const { - activePreset, - resetCounter, - toolsPanel, - previewPanel, - mainContent, - timeline, - setToolsPanel, - setPreviewPanel, - setMainContent, - setTimeline, - propertiesPanel, - setPropertiesPanel, - } = usePanelStore(); +function EditorLayout() { + const { panels, setPanel } = usePanelStore(); - return activePreset === "media" ? ( + return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ) : activePreset === "inspector" ? ( - - setPropertiesPanel(100 - size)} - className="min-h-0 min-w-0" - > - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ) : activePreset === "vertical-preview" ? ( - - setPreviewPanel(100 - size)} - className="min-h-0 min-w-0" - > - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ) : ( - { + setPanel("mainContent", sizes[0] ?? panels.mainContent); + setPanel("timeline", sizes[1] ?? panels.timeline); + }} > { + setPanel("tools", sizes[0] ?? panels.tools); + setPanel("preview", sizes[1] ?? panels.preview); + setPanel("properties", sizes[2] ?? panels.properties); + }} > @@ -307,9 +73,8 @@ function EditorLayout({}: {}) { @@ -318,10 +83,9 @@ function EditorLayout({}: {}) { @@ -332,10 +96,9 @@ function EditorLayout({}: {}) { diff --git a/apps/web/src/components/editor/assets-panel/views/captions.tsx b/apps/web/src/components/editor/assets-panel/views/captions.tsx index a44b5df0..fbcfcc2c 100644 --- a/apps/web/src/components/editor/assets-panel/views/captions.tsx +++ b/apps/web/src/components/editor/assets-panel/views/captions.tsx @@ -3,8 +3,7 @@ import { PropertyGroup } from "../../properties-panel/property-item"; import { PanelBaseView as BaseView } from "@/components/editor/panel-base-view"; import { LanguageSelect } from "@/components/language-select"; import { useState, useRef, useEffect } from "react"; -import { extractTimelineAudio } from "@/lib/mediabunny-utils"; -import { encryptWithRandomKey, arrayBufferToBase64 } from "@/lib/zk-encryption"; +import { extractTimelineAudio } from "@/lib/media/mediabunny"; import { useEditor } from "@/hooks/use-editor"; import { DEFAULT_TEXT_ELEMENT } from "@/constants/text-constants"; import { LANGUAGES } from "@/constants/captions-constants"; @@ -136,8 +135,8 @@ export function Captions() { }); shortCaptions.forEach((caption, index) => { - editor.timeline.addElementToTrack({ - trackId: captionTrackId, + editor.timeline.insertElement({ + placement: { mode: "explicit", trackId: captionTrackId }, element: { ...DEFAULT_TEXT_ELEMENT, name: `Caption ${index + 1}`, diff --git a/apps/web/src/components/editor/assets-panel/views/media.tsx b/apps/web/src/components/editor/assets-panel/views/media.tsx index 4d80cfc5..123ef782 100644 --- a/apps/web/src/components/editor/assets-panel/views/media.tsx +++ b/apps/web/src/components/editor/assets-panel/views/media.tsx @@ -11,16 +11,16 @@ import { Music, Video, } from "lucide-react"; +import type { LucideIcon } from "lucide-react"; import { toast } from "sonner"; import { useEditor } from "@/hooks/use-editor"; import { useFileUpload } from "@/hooks/use-file-upload"; import { useRevealItem } from "@/hooks/use-reveal-item"; -import { processMediaAssets } from "@/lib/media-processing-utils"; +import { processMediaAssets } from "@/lib/media/processing"; +import { cn } from "@/lib/utils"; import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants"; -import { canElementGoOnTrack } from "@/lib/timeline/track-utils"; -import { wouldElementOverlap } from "@/lib/timeline/element-utils"; import type { MediaAsset } from "@/types/assets"; -import type { CreateTimelineElement, TrackType } from "@/types/timeline"; +import type { CreateTimelineElement } from "@/types/timeline"; import { Button } from "@/components/ui/button"; import { MediaDragOverlay } from "@/components/editor/assets-panel/drag-overlay"; import { @@ -42,7 +42,6 @@ import { TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; -import { usePanelStore } from "@/stores/panel-store"; import { useAssetsPanelStore } from "@/stores/assets-panel-store"; export function MediaView() { @@ -50,8 +49,7 @@ export function MediaView() { const mediaFiles = editor.media.getAssets(); const activeProject = editor.project.getActive(); - const { mediaViewMode, setMediaViewMode } = usePanelStore(); - const { highlightMediaId, clearHighlight } = useAssetsPanelStore(); + const { mediaViewMode, setMediaViewMode, highlightMediaId, clearHighlight } = useAssetsPanelStore(); const { highlightedId, registerElement } = useRevealItem( highlightMediaId, clearHighlight, @@ -129,39 +127,9 @@ export function MediaView() { startTime: number; }): boolean => { const element = createElementFromMedia({ asset, startTime }); - const trackType = getTrackTypeForMedia({ mediaType: asset.type }); - const tracks = editor.timeline.getTracks(); - const duration = - asset.duration ?? TIMELINE_CONSTANTS.DEFAULT_ELEMENT_DURATION; - - const existingTrack = tracks.find((track) => { - if ( - !canElementGoOnTrack({ - elementType: element.type, - trackType: track.type, - }) - ) { - return false; - } - return !wouldElementOverlap({ - elements: track.elements, - startTime, - endTime: startTime + duration, - }); - }); - - if (existingTrack) { - editor.timeline.addElementToTrack({ - trackId: existingTrack.id, - element, - }); - return true; - } - - const newTrackId = editor.timeline.addTrack({ type: trackType }); - editor.timeline.addElementToTrack({ - trackId: newTrackId, + editor.timeline.insertElement({ element, + placement: { mode: "auto" }, }); return true; }; @@ -531,12 +499,58 @@ function ListView({ ); } +const formatDuration = ({ duration }: { duration: number }) => { + const min = Math.floor(duration / 60); + const sec = Math.floor(duration % 60); + return `${min}:${sec.toString().padStart(2, "0")}`; +}; + +function MediaDurationBadge({ duration }: { duration?: number }) { + if (!duration) return null; + + return ( +
+ {formatDuration({ duration })} +
+ ); +} + +function MediaDurationLabel({ duration }: { duration?: number }) { + if (!duration) return null; + + return ( + {formatDuration({ duration })} + ); +} + +function MediaTypePlaceholder({ + icon: Icon, + label, + duration, + variant, +}: { + icon: LucideIcon; + label: string; + duration?: number; + variant: "muted" | "bordered"; +}) { + const iconClassName = cn("size-6", variant === "bordered" && "mb-1"); + + return ( +
+
+ ); +} + function MediaPreview({ item }: { item: MediaAsset }) { - const formatDuration = (duration: number) => { - const min = Math.floor(duration / 60); - const sec = Math.floor(duration % 60); - return `${min}:${sec.toString().padStart(2, "0")}`; - }; if (item.type === "image") { return ( @@ -564,47 +578,38 @@ function MediaPreview({ item }: { item: MediaAsset }) {
- {item.duration && ( -
- {formatDuration(item.duration)} -
- )} + ); } return ( -
-
+ ); } if (item.type === "audio") { return ( -
- - Audio - {item.duration && ( - - {formatDuration(item.duration)} - - )} -
+ ); } return ( -
- - Unknown -
+ ); } @@ -631,22 +636,6 @@ function SortMenuItem({ ); } -function getTrackTypeForMedia({ - mediaType, -}: { - mediaType: MediaAsset["type"]; -}): TrackType { - switch (mediaType) { - case "video": - case "image": - return "video"; - case "audio": - return "audio"; - default: - return "video"; - } -} - function createElementFromMedia({ asset, startTime, @@ -697,7 +686,6 @@ function createElementFromMedia({ trimEnd: 0, volume: 1, muted: false, - buffer: new AudioBuffer({ length: 1, sampleRate: 44100 }), }; default: throw new Error(`Unsupported media type: ${asset.type}`); diff --git a/apps/web/src/components/editor/assets-panel/views/settings.tsx b/apps/web/src/components/editor/assets-panel/views/settings.tsx index ff786527..72a6e1ca 100644 --- a/apps/web/src/components/editor/assets-panel/views/settings.tsx +++ b/apps/web/src/components/editor/assets-panel/views/settings.tsx @@ -124,14 +124,23 @@ function ProjectInfoView() { const currentCanvasSize = getCurrentCanvasSize({ activeProject }); const currentAspectRatio = dimensionToAspectRatio(currentCanvasSize); + const originalCanvasSize = activeProject.settings.originalCanvasSize ?? null; const presetIndex = findPresetIndexByAspectRatio({ presets: canvasPresets, targetAspectRatio: currentAspectRatio, }); - const selectedPresetIndex = - presetIndex !== -1 ? presetIndex.toString() : undefined; + const originalPresetValue = "original"; + const selectedPresetValue = + presetIndex !== -1 ? presetIndex.toString() : originalPresetValue; const handleAspectRatioChange = ({ value }: { value: string }) => { + if (value === originalPresetValue) { + const canvasSize = originalCanvasSize ?? currentCanvasSize; + editor.project.updateSettings({ + settings: { canvasSize }, + }); + return; + } const index = parseInt(value, 10); const preset = canvasPresets[index]; if (preset) { @@ -157,13 +166,14 @@ function ProjectInfoView() { Aspect ratio