mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
push
This commit is contained in:
+187
@@ -0,0 +1,187 @@
|
||||
## Detailed Chat Summary
|
||||
|
||||
### Phase 1: Audit & Analysis
|
||||
Started with a 1800-line Zustand timeline store doing EVERYTHING (core logic + UI state + history). Performed comprehensive audit identifying:
|
||||
- ~40 methods mixing core logic with UI orchestration
|
||||
- Inconsistent parameter patterns (some destructured, some not)
|
||||
- Redundant methods (`updateElementStartTime` vs `updateElementStartTimeWithRipple`)
|
||||
- Three separate split operations that should be unified
|
||||
- Delete operations conflating UI state with core logic
|
||||
- Methods referencing `selectedElements` (UI state) that shouldn't exist in core
|
||||
|
||||
### Phase 2: Architecture Decision
|
||||
Decided on Command Pattern + separation of concerns:
|
||||
- **Core Logic** → TimelineManager + Commands
|
||||
- **UI State** → Zustand store
|
||||
- **Ephemeral Interaction** → Component state (dragState)
|
||||
- **History Tracking** → HistoryManager
|
||||
|
||||
### Phase 3: HistoryManager Creation
|
||||
Created generic `HistoryManager<T>`:
|
||||
- `execute(command)` returns the command (enables getting return values)
|
||||
- Maintains `history` and `redoStack` arrays of Command objects
|
||||
- `undo()` pops from history, calls `command.undo()`
|
||||
- `redo()` pops from redoStack, calls `command.redo()`
|
||||
- Methods: `canUndo()`, `canRedo()`, `clear()`
|
||||
|
||||
### Phase 4: Command Architecture
|
||||
Created base `Command` abstract class in `src/lib/commands/base-command.ts`:
|
||||
- Abstract `execute()` method (must implement)
|
||||
- `undo()` throws by default (implement if reversible)
|
||||
- `redo()` defaults to calling `execute()` again
|
||||
|
||||
Built 15 command classes organized in folders:
|
||||
- **Track operations** (3): `AddTrackCommand`, `RemoveTrackCommand`, `ToggleTrackMuteCommand`
|
||||
- **Element CRUD** (5): `AddElementToTrackCommand`, `DeleteElementsCommand`, `DuplicateElementsCommand`, `UpdateElementTrimCommand`, `UpdateElementDurationCommand`
|
||||
- **Element properties** (4): `UpdateTextElementCommand`, `ToggleElementsHiddenCommand`, `ToggleElementsMutedCommand`, `SplitElementsCommand`
|
||||
- **Unified position** (1): `UpdateElementStartTimeCommand` (replaced 3 old methods)
|
||||
- **Clipboard** (1): `PasteCommand`
|
||||
|
||||
Each command:
|
||||
- Takes minimal parameters in constructor
|
||||
- Saves state before execution for undo
|
||||
- Implements `execute()` and `undo()`
|
||||
- Has getters for return values (e.g., `getTrackId()`)
|
||||
- Uses `EditorCore.getInstance()` to access timeline manager
|
||||
|
||||
### Phase 5: AddTrackCommand Details
|
||||
Updated `AddTrackCommand` to support optional `index` parameter (unified `addTrack` and `insertTrackAt`):
|
||||
- Constructor: `new AddTrackCommand(type, index?)`
|
||||
- Splices into array at index if provided, appends if not
|
||||
- Returns trackId via `getTrackId()` getter
|
||||
- Saves full state for undo
|
||||
|
||||
### Phase 6: TimelineManager Refactor - Part 1
|
||||
Removed ALL UI state fields:
|
||||
- ❌ `clipboard` → UI state
|
||||
- ❌ `snappingEnabled` → UI preference
|
||||
- ❌ `rippleEditingEnabled` → UI preference
|
||||
- ❌ `selectedElements` → UI selection
|
||||
- ❌ `dragState` → ephemeral interaction
|
||||
- ❌ `DragData` import (only used for UI state)
|
||||
|
||||
### Phase 7: TimelineManager Refactor - Part 2
|
||||
Updated method implementations to use commands:
|
||||
- `addTrack()` → `new AddTrackCommand(type, index).execute()`
|
||||
- `removeTrack()` → `new RemoveTrackCommand(trackId).execute()`
|
||||
- `updateElementTrim()` → creates command, conditionally pushes history
|
||||
- `updateElementDuration()` → creates command, conditionally pushes history
|
||||
- `updateElementStartTime()` → simplified (always through history)
|
||||
- `deleteElements()` → simplified (always through history)
|
||||
- `splitElements()` → `new SplitElementsCommand(elements, splitTime, retainSide).execute()`
|
||||
- `duplicateElements()`, `toggleElementsHidden()`, `toggleElementsMuted()` → via commands
|
||||
|
||||
### Phase 8: TimelineManager Cleanup
|
||||
Removed methods that referenced UI state:
|
||||
- ❌ `getSortedTracks()` (stub, redundant)
|
||||
- ❌ `toggleSnapping()` (UI preference)
|
||||
- ❌ `toggleRippleEditing()` (UI preference)
|
||||
- ❌ `selectElement()`, `deselectElement()`, etc. (UI selection)
|
||||
- ❌ `setDragState()`, `startDrag()`, `updateDragTime()`, `endDrag()` (UI interaction)
|
||||
- ❌ `copySelected()` (orchestrates UI state + core logic)
|
||||
- ❌ `deleteSelectedElements()` (uses UI state)
|
||||
- ❌ `splitSelected()` (uses UI state)
|
||||
- ❌ `toggleSelectedHidden()`, `toggleSelectedMuted()` (use UI state)
|
||||
- ❌ `revealElementInMedia()` (UI navigation)
|
||||
- ❌ `getContextMenuState()` (UI query)
|
||||
- ❌ `addElementAtTime()`, `addElementToNewTrack()` (convenience wrappers)
|
||||
|
||||
**Result:** TimelineManager reduced from 1800 → 315 lines, now pure core logic only
|
||||
|
||||
### Phase 9: Timeline Store Refactor
|
||||
Completely rewrote store (1800 → 104 lines) to contain ONLY UI state:
|
||||
```typescript
|
||||
interface TimelineStore {
|
||||
// Selection
|
||||
selectedElements: { trackId: string; elementId: string }[]
|
||||
selectElement()
|
||||
deselectElement()
|
||||
clearSelectedElements()
|
||||
setSelectedElements()
|
||||
|
||||
// Preferences
|
||||
snappingEnabled: boolean
|
||||
toggleSnapping()
|
||||
|
||||
rippleEditingEnabled: boolean
|
||||
toggleRippleEditing()
|
||||
|
||||
// Clipboard
|
||||
clipboard: { items: Array<...> } | null
|
||||
setClipboard()
|
||||
}
|
||||
```
|
||||
|
||||
Removed all methods that:
|
||||
- Operated on `_tracks`
|
||||
- Managed history
|
||||
- Used commands
|
||||
- Implemented core timeline logic
|
||||
|
||||
### Phase 10: Identified dragState Problem
|
||||
Analyzed `dragState` usage across three files:
|
||||
- **timeline-track.tsx** (producer): Sets/updates drag state on mouse events
|
||||
- **timeline-element.tsx** (consumer): Reads `dragState.elementId` and `dragState.currentTime` for visual feedback during drag
|
||||
- **index.tsx** (consumer): Reads `dragState.isDragging` to show snap indicator
|
||||
|
||||
**Problem:** In global Zustand store (wrong scope)
|
||||
- Only used during active drag operations
|
||||
- Ephemeral state (cleared after drag ends)
|
||||
- Causes unnecessary re-renders across entire app
|
||||
- Only needs to exist in Timeline component tree
|
||||
|
||||
**Solution:** Move to Timeline component state:
|
||||
- Keep `dragState` as local state in `Timeline` component
|
||||
- Pass down via props to `TimelineTrack` and `TimelineElement`
|
||||
- Timeline component manages `handleMouseDown`, `handleMouseMove`, `handleMouseUp` in TimelineTrack
|
||||
- This keeps scope correct and re-renders localized
|
||||
|
||||
### Phase 11: Architectural Alignment
|
||||
Confirmed final architecture:
|
||||
- **Editor Core** (singleton):
|
||||
- `history: HistoryManager` (manages undo/redo)
|
||||
- `timeline: TimelineManager` (pure data + commands)
|
||||
- Other managers (project, media, scene, playback)
|
||||
|
||||
- **Timeline Component** (React):
|
||||
- `dragState` (local state via useState)
|
||||
- Props down to children
|
||||
|
||||
- **Timeline Store** (Zustand):
|
||||
- Selection state + methods
|
||||
- Preferences (snapping, ripple editing)
|
||||
- Clipboard state
|
||||
|
||||
### Phase 12: Command Pattern Benefits Identified
|
||||
1. **Self-contained operations** - Each command knows how to undo itself
|
||||
2. **Discoverable API** - All timeline actions are in `/lib/commands/timeline/`
|
||||
3. **Type-safe** - Commands are strongly typed
|
||||
4. **Testable** - Each command can be unit tested independently
|
||||
5. **Scalable** - Adding new operations = creating new command class
|
||||
6. **Clean manager** - TimelineManager becomes orchestrator, not logic holder
|
||||
7. **Reusable** - Commands can be executed from anywhere (UI, scripts, etc.)
|
||||
|
||||
### Summary of Changes Made
|
||||
|
||||
**Files Created:**
|
||||
- `src/lib/commands/base-command.ts` (12 lines)
|
||||
- `src/lib/commands/index.ts` (3 lines)
|
||||
- `src/lib/commands/timeline/index.ts` (4 lines)
|
||||
- `src/lib/commands/timeline/track/` (3 commands)
|
||||
- `src/lib/commands/timeline/element/` (9 commands)
|
||||
- `src/lib/commands/timeline/clipboard/` (1 command)
|
||||
|
||||
**Files Modified:**
|
||||
- `src/core/managers/history-manager.ts` - Refactored to work with Commands, now returns command
|
||||
- `src/core/managers/timeline-manager.ts` - Removed UI state, implemented all methods as command-driven
|
||||
- `src/stores/timeline-store.ts` - From 1800 → 104 lines, now UI state only
|
||||
|
||||
**Files Deleted:**
|
||||
- Old history logic removed (consolidated into HistoryManager)
|
||||
- Removed unused imports from components (cascading changes needed)
|
||||
|
||||
### Remaining Work
|
||||
1. Move `dragState` from store to Timeline component
|
||||
2. Update all component imports and method calls
|
||||
3. Implement remaining stub methods (`checkElementOverlap`, `findOrCreateTrack`, etc.)
|
||||
4. Wire up UI to call new manager methods instead of store methods
|
||||
Reference in New Issue
Block a user