Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
93d1e3383c | ||
|
|
fca99d6126 | ||
|
|
33a8705f0d | ||
|
|
fe10cee3eb | ||
|
|
1d05f5ac56 | ||
|
|
395e415cc4 | ||
|
|
e50a817b97 | ||
|
|
484ce47867 | ||
|
|
681f006b35 | ||
|
|
0282ef2223 | ||
|
|
dc0b57cd6a | ||
|
|
a218a52d2d | ||
|
|
1c2cf80188 | ||
|
|
cdd29eb1dc | ||
|
|
34cfeacdbb | ||
|
|
6d55a34586 | ||
|
|
c6666fd385 | ||
|
|
9b5fd6eb07 | ||
|
|
00cf7f6ab7 | ||
|
|
a82817a703 | ||
|
|
39b6ea5613 | ||
|
|
ae45ef8154 | ||
|
|
49c4fcb229 | ||
|
|
464a6e83b9 | ||
|
|
d0b4b1a5fc | ||
|
|
2257ca2e6f | ||
|
|
490fc170c0 | ||
|
|
feb8a07c16 | ||
|
|
7366cb27e9 | ||
|
|
34daad9df7 | ||
|
|
9bc35363b9 | ||
|
|
e84c0cfda6 | ||
|
|
1dbc22b2f4 | ||
|
|
ad794fa43b | ||
|
|
9ca32e5e39 | ||
|
|
5120e2efa6 | ||
|
|
1aec9c5cf5 | ||
|
|
e88a059222 | ||
|
|
c3f0d640ef |
@@ -49,7 +49,7 @@ Review every point below carefully to ensure files follow consistent code style
|
|||||||
|
|
||||||
- [ ] Readability over brevity — use `element` not `el`, `event` not `e`
|
- [ ] Readability over brevity — use `element` not `el`, `event` not `e`
|
||||||
- [ ] Booleans are named `isSomething`, `hasSomething`, or `shouldSomething` — not `something`
|
- [ ] Booleans are named `isSomething`, `hasSomething`, or `shouldSomething` — not `something`
|
||||||
- [ ] No title case in text/UI — use `Hello world` not `Hello World`
|
- [ ] No title case for multi-word text/UI — use `Hello world` not `Hello World`
|
||||||
|
|
||||||
## Tailwind & Styling
|
## Tailwind & Styling
|
||||||
|
|
||||||
@@ -61,12 +61,15 @@ Review every point below carefully to ensure files follow consistent code style
|
|||||||
<PlusIcon className="!size-6" /> {/* ✅ correct */}
|
<PlusIcon className="!size-6" /> {/* ✅ correct */}
|
||||||
<PlusIcon className="size-6" /> {/* ❌ wrong */}
|
<PlusIcon className="size-6" /> {/* ❌ wrong */}
|
||||||
<PlusIcon className="!size-4" /> {/* ❌ unnecessary, size-4 is default */}
|
<PlusIcon className="!size-4" /> {/* ❌ unnecessary, size-4 is default */}
|
||||||
|
<PlusIcon className="size-4" />{" "}
|
||||||
|
{/* ❌ completely wrong, 1) doesn't override and 2) size-4 is default */}
|
||||||
</Button>
|
</Button>
|
||||||
```
|
```
|
||||||
|
|
||||||
## State Management (Zustand)
|
## State Management (Zustand)
|
||||||
|
|
||||||
- [ ] React components never use `someStore.getState()` — use the `useSomeStore` hook instead
|
- [ ] React components never use `someStore.getState()` — use the `useSomeStore` hook instead
|
||||||
|
- [ ] High-frequency stores (timeline, playback, selections) use selectors — `useStore((s) => s.value)` not `const { value } = useStore()`
|
||||||
- [ ] Store/manager methods are not passed as props — sub-components access them directly
|
- [ ] Store/manager methods are not passed as props — sub-components access them directly
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
@@ -91,6 +94,21 @@ Review every point below carefully to ensure files follow consistent code style
|
|||||||
|
|
||||||
- [ ] Code is scannable — use variables and helper functions to make intent clear at a glance
|
- [ ] Code is scannable — use variables and helper functions to make intent clear at a glance
|
||||||
- [ ] Complex logic is extracted into well-named variables or helpers
|
- [ ] Complex logic is extracted into well-named variables or helpers
|
||||||
|
- [ ] No magic numbers or magic values — extract inline literals into named constants
|
||||||
|
- Applies to colors, durations, thresholds, sizes, config values, etc.
|
||||||
|
- If it's domain-specific to one file, a `const` at the top of that file is fine
|
||||||
|
- If it's generic enough, it belongs in `constants/`
|
||||||
|
|
||||||
|
- [ ] No redundant single/plural function variants — if a function can operate on multiple items, it should accept an array and handle both cases. Don't create `doThing()` + `doThings()`.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// ❌ wrong — redundant variants
|
||||||
|
function updateElement({ element }: { element: Element }) { ... }
|
||||||
|
function updateElements({ elements }: { elements: Element[] }) { ... }
|
||||||
|
|
||||||
|
// ✅ correct — one function, accepts array
|
||||||
|
function updateElements({ elements }: { elements: Element[] }) { ... }
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -106,4 +124,25 @@ Review every point below carefully to ensure files follow consistent code style
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
> Every decision, every edit must be carefully considered. Everything matters.
|
## Review Methodology
|
||||||
|
|
||||||
|
Do NOT review by reading the file top-to-bottom and noting what jumps out. Instead:
|
||||||
|
|
||||||
|
1. Go through each checklist section **one at a time**
|
||||||
|
2. For each section, scan the **entire file** for violations of that specific rule
|
||||||
|
3. Only move to the next section after you've exhausted the current one
|
||||||
|
4. After all sections are checked, do a final pass: re-read every checklist item and confirm you didn't skip it
|
||||||
|
|
||||||
|
Before outputting the table, list each checklist section and confirm you checked it:
|
||||||
|
`Signatures ✓ | TypeScript ✓ | JSX ✓ | Organization ✓ | Comments ✓ | Naming ✓ | Tailwind ✓ | State ✓ | Quality ✓ | Keywords ✓`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## IMPORTANT: Review Rules
|
||||||
|
|
||||||
|
- **ONLY** flag issues that are explicitly covered by a checklist item above.
|
||||||
|
- Do **NOT** invent your own rules, suggestions, or "nice to haves" as primary issues.
|
||||||
|
- The review output must be a table of issues, each one mapping to a specific checklist item.
|
||||||
|
- If something looks off but isn't covered by the checklist, you can mention it as a brief side note at the end — but keep it clearly separate from the actual review. Always default to fixing the issues covered by the checklist above, unless the user says otherwise.
|
||||||
|
|
||||||
|
> You WILL miss things if you try to review the whole file in one pass. Iterate rule by rule.
|
||||||
|
|||||||
@@ -1,603 +0,0 @@
|
|||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":2.2,"tier":2,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":111,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993064304}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":2,"cachedCount":0},"timestamp":1769993064304}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"D","location":"use-filmstrip.ts:114","message":"Requesting missing filmstrip frames","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":2,"missingCount":111,"newCount":111,"firstNewTimestamp":0,"lastNewTimestamp":55},"timestamp":1769993064305}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":2.2,"tier":2,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":111,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993064309}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":2,"cachedCount":0},"timestamp":1769993064309}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H","location":"worker.ts:62","message":"Filmstrip extract request","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","fileType":"","fileSize":3787343,"timestampCount":111,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993064641}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":0.13849909222110923,"tier":0,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":12,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993066624}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":0,"cachedCount":0},"timestamp":1769993066625}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":37.5,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993096424}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":0},"timestamp":1769993096424}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"D","location":"use-filmstrip.ts:114","message":"Requesting missing filmstrip frames","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"missingCount":221,"newCount":110,"firstNewTimestamp":0.25,"lastNewTimestamp":54.75},"timestamp":1769993096425}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H","location":"worker.ts:62","message":"Filmstrip extract request","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","fileType":"","fileSize":3787343,"timestampCount":110,"firstTimestamp":0.25,"lastTimestamp":54.75},"timestamp":1769993096425}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"K","location":"worker.ts:104","message":"Requesting sample for timestamp","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","timestamp":0.25},"timestamp":1769993096427}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"L","location":"worker.ts:132","message":"Sample received","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","timestamp":0.25},"timestamp":1769993096451}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"J","location":"worker.ts:131","message":"Frame encoded to dataUrl","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","timestamp":0.25,"dataUrlLength":3111,"dataUrlPrefix":"data:image/jpeg;base64,/9j/4AAQSkZJRgABA"},"timestamp":1769993096460}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"E","location":"use-filmstrip.ts:146","message":"Filmstrip frame received","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"timestamp":0.25,"updatedCount":1,"totalExpected":221},"timestamp":1769993096461}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"F","location":"filmstrip.tsx:58","message":"Filmstrip rendered with single frame","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","framesCount":1,"firstTimestamp":0.25,"thumbnailWidth":107,"pixelsPerSecond":1875,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"containerWidth":103227},"timestamp":1769993096463}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"E","location":"use-filmstrip.ts:146","message":"Filmstrip frame received","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"timestamp":0.75,"updatedCount":2,"totalExpected":221},"timestamp":1769993096486}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"G","location":"filmstrip.tsx:86","message":"Filmstrip rendered with multiple frames","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","framesCount":2,"firstTimestamp":0.25,"lastTimestamp":0.75,"thumbnailWidth":107,"pixelsPerSecond":1875,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"containerWidth":103227},"timestamp":1769993096487}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"E","location":"use-filmstrip.ts:146","message":"Filmstrip frame received","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"timestamp":1.25,"updatedCount":3,"totalExpected":221},"timestamp":1769993096550}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"E","location":"use-filmstrip.ts:146","message":"Filmstrip frame received","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"timestamp":1.75,"updatedCount":4,"totalExpected":221},"timestamp":1769993096601}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"E","location":"use-filmstrip.ts:146","message":"Filmstrip frame received","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"timestamp":2.25,"updatedCount":5,"totalExpected":221},"timestamp":1769993096670}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"E","location":"use-filmstrip.ts:146","message":"Filmstrip frame received","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"timestamp":2.75,"updatedCount":6,"totalExpected":221},"timestamp":1769993096731}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"E","location":"use-filmstrip.ts:146","message":"Filmstrip frame received","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"timestamp":3.25,"updatedCount":7,"totalExpected":221},"timestamp":1769993096742}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"E","location":"use-filmstrip.ts:146","message":"Filmstrip frame received","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"timestamp":3.75,"updatedCount":8,"totalExpected":221},"timestamp":1769993096760}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"E","location":"use-filmstrip.ts:146","message":"Filmstrip frame received","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"timestamp":4.25,"updatedCount":9,"totalExpected":221},"timestamp":1769993096789}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"E","location":"use-filmstrip.ts:146","message":"Filmstrip frame received","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"timestamp":4.75,"updatedCount":10,"totalExpected":221},"timestamp":1769993096826}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"E","location":"use-filmstrip.ts:146","message":"Filmstrip frame received","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"timestamp":5.25,"updatedCount":11,"totalExpected":221},"timestamp":1769993096871}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"E","location":"use-filmstrip.ts:146","message":"Filmstrip frame received","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"timestamp":5.75,"updatedCount":12,"totalExpected":221},"timestamp":1769993096919}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"E","location":"use-filmstrip.ts:146","message":"Filmstrip frame received","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"timestamp":6.25,"updatedCount":13,"totalExpected":221},"timestamp":1769993096929}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"E","location":"use-filmstrip.ts:146","message":"Filmstrip frame received","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"timestamp":6.75,"updatedCount":14,"totalExpected":221},"timestamp":1769993096945}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"E","location":"use-filmstrip.ts:146","message":"Filmstrip frame received","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"timestamp":7.25,"updatedCount":15,"totalExpected":221},"timestamp":1769993096967}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"E","location":"use-filmstrip.ts:146","message":"Filmstrip frame received","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"timestamp":7.75,"updatedCount":16,"totalExpected":221},"timestamp":1769993096994}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"E","location":"use-filmstrip.ts:146","message":"Filmstrip frame received","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"timestamp":8.25,"updatedCount":17,"totalExpected":221},"timestamp":1769993097031}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"E","location":"use-filmstrip.ts:146","message":"Filmstrip frame received","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"timestamp":8.75,"updatedCount":18,"totalExpected":221},"timestamp":1769993097073}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"E","location":"use-filmstrip.ts:146","message":"Filmstrip frame received","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"timestamp":9.25,"updatedCount":19,"totalExpected":221},"timestamp":1769993097081}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"E","location":"use-filmstrip.ts:146","message":"Filmstrip frame received","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"timestamp":9.75,"updatedCount":20,"totalExpected":221},"timestamp":1769993097095}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"E","location":"use-filmstrip.ts:146","message":"Filmstrip frame received","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"timestamp":10.25,"updatedCount":21,"totalExpected":221},"timestamp":1769993097117}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"E","location":"use-filmstrip.ts:146","message":"Filmstrip frame received","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"timestamp":10.75,"updatedCount":22,"totalExpected":221},"timestamp":1769993097145}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"E","location":"use-filmstrip.ts:146","message":"Filmstrip frame received","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"timestamp":11.25,"updatedCount":23,"totalExpected":221},"timestamp":1769993097180}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"E","location":"use-filmstrip.ts:146","message":"Filmstrip frame received","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"timestamp":11.75,"updatedCount":24,"totalExpected":221},"timestamp":1769993097222}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"E","location":"use-filmstrip.ts:146","message":"Filmstrip frame received","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"timestamp":12.25,"updatedCount":25,"totalExpected":221},"timestamp":1769993097231}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"E","location":"use-filmstrip.ts:146","message":"Filmstrip frame received","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"timestamp":12.75,"updatedCount":26,"totalExpected":221},"timestamp":1769993097271}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"E","location":"use-filmstrip.ts:146","message":"Filmstrip frame received","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"timestamp":13.25,"updatedCount":27,"totalExpected":221},"timestamp":1769993097293}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"E","location":"use-filmstrip.ts:146","message":"Filmstrip frame received","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"timestamp":13.75,"updatedCount":28,"totalExpected":221},"timestamp":1769993097322}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"E","location":"use-filmstrip.ts:146","message":"Filmstrip frame received","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"timestamp":14.25,"updatedCount":29,"totalExpected":221},"timestamp":1769993097364}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"E","location":"use-filmstrip.ts:146","message":"Filmstrip frame received","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"timestamp":14.75,"updatedCount":30,"totalExpected":221},"timestamp":1769993097406}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"E","location":"use-filmstrip.ts:146","message":"Filmstrip frame received","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"timestamp":15.25,"updatedCount":31,"totalExpected":221},"timestamp":1769993097414}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"E","location":"use-filmstrip.ts:146","message":"Filmstrip frame received","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"timestamp":15.75,"updatedCount":32,"totalExpected":221},"timestamp":1769993097429}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"E","location":"use-filmstrip.ts:146","message":"Filmstrip frame received","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"timestamp":16.25,"updatedCount":33,"totalExpected":221},"timestamp":1769993097450}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"E","location":"use-filmstrip.ts:146","message":"Filmstrip frame received","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"timestamp":16.75,"updatedCount":34,"totalExpected":221},"timestamp":1769993097479}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"E","location":"use-filmstrip.ts:146","message":"Filmstrip frame received","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"timestamp":17.25,"updatedCount":35,"totalExpected":221},"timestamp":1769993097514}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"E","location":"use-filmstrip.ts:146","message":"Filmstrip frame received","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"timestamp":17.75,"updatedCount":36,"totalExpected":221},"timestamp":1769993097566}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"E","location":"use-filmstrip.ts:146","message":"Filmstrip frame received","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"timestamp":18.25,"updatedCount":37,"totalExpected":221},"timestamp":1769993097574}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"E","location":"use-filmstrip.ts:146","message":"Filmstrip frame received","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"timestamp":18.75,"updatedCount":38,"totalExpected":221},"timestamp":1769993097589}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"E","location":"use-filmstrip.ts:146","message":"Filmstrip frame received","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"timestamp":19.25,"updatedCount":39,"totalExpected":221},"timestamp":1769993097610}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"E","location":"use-filmstrip.ts:146","message":"Filmstrip frame received","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"timestamp":19.75,"updatedCount":40,"totalExpected":221},"timestamp":1769993097639}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"E","location":"use-filmstrip.ts:146","message":"Filmstrip frame received","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"timestamp":20.25,"updatedCount":41,"totalExpected":221},"timestamp":1769993097672}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"E","location":"use-filmstrip.ts:146","message":"Filmstrip frame received","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"timestamp":20.75,"updatedCount":42,"totalExpected":221},"timestamp":1769993097713}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"E","location":"use-filmstrip.ts:146","message":"Filmstrip frame received","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"timestamp":21.25,"updatedCount":43,"totalExpected":221},"timestamp":1769993097721}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"E","location":"use-filmstrip.ts:146","message":"Filmstrip frame received","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"timestamp":21.75,"updatedCount":44,"totalExpected":221},"timestamp":1769993097735}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"E","location":"use-filmstrip.ts:146","message":"Filmstrip frame received","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"timestamp":22.25,"updatedCount":45,"totalExpected":221},"timestamp":1769993097757}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"E","location":"use-filmstrip.ts:146","message":"Filmstrip frame received","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"timestamp":22.75,"updatedCount":46,"totalExpected":221},"timestamp":1769993097785}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"E","location":"use-filmstrip.ts:146","message":"Filmstrip frame received","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"timestamp":23.25,"updatedCount":47,"totalExpected":221},"timestamp":1769993097820}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"E","location":"use-filmstrip.ts:146","message":"Filmstrip frame received","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"timestamp":23.75,"updatedCount":48,"totalExpected":221},"timestamp":1769993097861}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"E","location":"use-filmstrip.ts:146","message":"Filmstrip frame received","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"timestamp":24.25,"updatedCount":49,"totalExpected":221},"timestamp":1769993097870}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"E","location":"use-filmstrip.ts:146","message":"Filmstrip frame received","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"timestamp":24.75,"updatedCount":50,"totalExpected":221},"timestamp":1769993097884}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"E","location":"use-filmstrip.ts:146","message":"Filmstrip frame received","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"timestamp":25.25,"updatedCount":51,"totalExpected":221},"timestamp":1769993097910}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"E","location":"use-filmstrip.ts:146","message":"Filmstrip frame received","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"timestamp":25.75,"updatedCount":52,"totalExpected":221},"timestamp":1769993097936}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"E","location":"use-filmstrip.ts:146","message":"Filmstrip frame received","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"timestamp":26.25,"updatedCount":53,"totalExpected":221},"timestamp":1769993097971}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":19.9,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993098068}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993098068}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":18.8,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993168875}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993168875}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":0.13849909222110923,"tier":0,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":12,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993168900}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":0,"cachedCount":0},"timestamp":1769993168901}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":0.15234900144322017,"tier":0,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":12,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993192703}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":0,"cachedCount":0},"timestamp":1769993192704}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":0.1675839015875422,"tier":0,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":12,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993192722}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":0,"cachedCount":0},"timestamp":1769993192722}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":0.18434229174629643,"tier":0,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":12,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993192738}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":0,"cachedCount":0},"timestamp":1769993192738}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":0.2027765209209261,"tier":0,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":12,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993192752}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":0,"cachedCount":0},"timestamp":1769993192753}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":0.22305417301301872,"tier":0,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":12,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993192770}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":0,"cachedCount":0},"timestamp":1769993192771}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":0.2453595903143206,"tier":0,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":12,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993192804}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":0,"cachedCount":0},"timestamp":1769993192804}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":0.2698955493457527,"tier":0,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":12,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993193123}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":0,"cachedCount":0},"timestamp":1769993193123}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":0.296885104280328,"tier":0,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":12,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993193131}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":0,"cachedCount":0},"timestamp":1769993193132}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":0.3265736147083608,"tier":0,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":12,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993193145}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":0,"cachedCount":0},"timestamp":1769993193146}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":0.3592309761791969,"tier":0,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":12,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993193163}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":0,"cachedCount":0},"timestamp":1769993193163}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":0.39515407379711665,"tier":0,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":12,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993193195}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":0,"cachedCount":0},"timestamp":1769993193195}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":0.43466948117682835,"tier":0,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":12,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993193484}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":0,"cachedCount":0},"timestamp":1769993193485}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":0.4781364292945112,"tier":0,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":12,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993193497}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":0,"cachedCount":0},"timestamp":1769993193497}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":0.5259500722239624,"tier":1,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":56,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993193513}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":1,"cachedCount":0},"timestamp":1769993193513}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":0.5785450794463587,"tier":1,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":56,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993193529}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":1,"cachedCount":0},"timestamp":1769993193530}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":0.6363995873909946,"tier":1,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":56,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993193921}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":1,"cachedCount":0},"timestamp":1769993193922}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":0.7000395461300941,"tier":1,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":56,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993193941}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":1,"cachedCount":0},"timestamp":1769993193942}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":0.7700435007431036,"tier":1,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":56,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993193964}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":1,"cachedCount":0},"timestamp":1769993193964}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":0.847047850817414,"tier":1,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":56,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993193988}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":1,"cachedCount":0},"timestamp":1769993193989}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":1.0249278994890711,"tier":1,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":56,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993194010}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":1,"cachedCount":0},"timestamp":1769993194011}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":1.1274206894379784,"tier":1,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":56,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993194030}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":1,"cachedCount":0},"timestamp":1769993194030}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":1.2401627583817763,"tier":1,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":56,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993194248}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":1,"cachedCount":0},"timestamp":1769993194249}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":1.364179034219954,"tier":1,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":56,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993194266}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":1,"cachedCount":0},"timestamp":1769993194266}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":1.5005969376419495,"tier":1,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":56,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993194282}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":1,"cachedCount":0},"timestamp":1769993194282}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":1.6506566314061446,"tier":1,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":56,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993194300}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":1,"cachedCount":0},"timestamp":1769993194300}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":1.8157222945467593,"tier":1,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":56,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993194316}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":1,"cachedCount":0},"timestamp":1769993194316}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":1.9972945240014353,"tier":1,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":56,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993194564}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":1,"cachedCount":0},"timestamp":1769993194565}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":2.197023976401579,"tier":2,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":111,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993194586}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":2,"cachedCount":0},"timestamp":1769993194586}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":2.416726374041737,"tier":2,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":111,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993194602}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":2,"cachedCount":0},"timestamp":1769993194603}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":2.6583990114459106,"tier":2,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":111,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993194618}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":2,"cachedCount":0},"timestamp":1769993194619}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":2.416726374041737,"tier":2,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":111,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993202785}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":2,"cachedCount":0},"timestamp":1769993202785}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":2.197023976401579,"tier":2,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":111,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993202816}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":2,"cachedCount":0},"timestamp":1769993202817}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":1.8157222945467593,"tier":1,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":56,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993202841}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":1,"cachedCount":0},"timestamp":1769993202841}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":1.6506566314061448,"tier":1,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":56,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993203091}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":1,"cachedCount":0},"timestamp":1769993203091}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":1.50059693764195,"tier":1,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":56,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993203103}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":1,"cachedCount":0},"timestamp":1769993203103}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":1.3641790342199545,"tier":1,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":56,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993203113}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":1,"cachedCount":0},"timestamp":1769993203114}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":1.2401627583817767,"tier":1,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":56,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993203130}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":1,"cachedCount":0},"timestamp":1769993203130}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":1.1274206894379788,"tier":1,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":56,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993203148}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":1,"cachedCount":0},"timestamp":1769993203149}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":1.0249278994890716,"tier":1,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":56,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993203365}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":1,"cachedCount":0},"timestamp":1769993203365}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":0.9317526358991559,"tier":1,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":56,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993203379}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":1,"cachedCount":0},"timestamp":1769993203379}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":0.8470478508174144,"tier":1,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":56,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993203396}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":1,"cachedCount":0},"timestamp":1769993203396}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":0.770043500743104,"tier":1,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":56,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993203414}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":1,"cachedCount":0},"timestamp":1769993203414}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":0.7000395461300946,"tier":1,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":56,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993203662}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":1,"cachedCount":0},"timestamp":1769993203662}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":0.636399587390995,"tier":1,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":56,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993203678}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":1,"cachedCount":0},"timestamp":1769993203678}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":0.578545079446359,"tier":1,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":56,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993203695}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":1,"cachedCount":0},"timestamp":1769993203696}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":0.5259500722239627,"tier":1,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":56,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993203728}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":1,"cachedCount":0},"timestamp":1769993203728}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":0.578545079446359,"tier":1,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":56,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993204155}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":1,"cachedCount":0},"timestamp":1769993204155}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":0.636399587390995,"tier":1,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":56,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993204173}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":1,"cachedCount":0},"timestamp":1769993204173}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":0.7000395461300946,"tier":1,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":56,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993204191}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":1,"cachedCount":0},"timestamp":1769993204192}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":0.7700435007431041,"tier":1,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":56,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993204212}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":1,"cachedCount":0},"timestamp":1769993204212}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":0.7000395461300947,"tier":1,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":56,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993205253}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":1,"cachedCount":0},"timestamp":1769993205253}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":0.6363995873909951,"tier":1,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":56,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993205270}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":1,"cachedCount":0},"timestamp":1769993205271}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":0.5785450794463591,"tier":1,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":56,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993205290}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":1,"cachedCount":0},"timestamp":1769993205290}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":0.5259500722239628,"tier":1,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":56,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993205309}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":1,"cachedCount":0},"timestamp":1769993205309}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":0.47813642929451167,"tier":0,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":12,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993205329}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":0,"cachedCount":0},"timestamp":1769993205329}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":0.5259500722239628,"tier":1,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":56,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993205721}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":1,"cachedCount":0},"timestamp":1769993205722}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":0.5785450794463591,"tier":1,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":56,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993205732}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":1,"cachedCount":0},"timestamp":1769993205732}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":0.6363995873909951,"tier":1,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":56,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993205745}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":1,"cachedCount":0},"timestamp":1769993205746}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":0.7000395461300947,"tier":1,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":56,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993205762}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":1,"cachedCount":0},"timestamp":1769993205762}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":0.7700435007431042,"tier":1,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":56,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993205780}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":1,"cachedCount":0},"timestamp":1769993205780}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":0.8470478508174147,"tier":1,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":56,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993205797}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":1,"cachedCount":0},"timestamp":1769993205797}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":0.9317526358991562,"tier":1,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":56,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993205846}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":1,"cachedCount":0},"timestamp":1769993205846}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":1.024927899489072,"tier":1,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":56,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993206065}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":1,"cachedCount":0},"timestamp":1769993206066}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":1.1274206894379792,"tier":1,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":56,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993206082}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":1,"cachedCount":0},"timestamp":1769993206082}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":1.2401627583817774,"tier":1,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":56,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993206099}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":1,"cachedCount":0},"timestamp":1769993206099}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":1.3641790342199551,"tier":1,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":56,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993206115}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":1,"cachedCount":0},"timestamp":1769993206116}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":1.5005969376419508,"tier":1,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":56,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993206131}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":1,"cachedCount":0},"timestamp":1769993206131}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":1.650656631406146,"tier":1,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":56,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993206375}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":1,"cachedCount":0},"timestamp":1769993206375}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":1.8157222945467606,"tier":1,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":56,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993206398}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":1,"cachedCount":0},"timestamp":1769993206398}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":1.9972945240014368,"tier":1,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":56,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993206420}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":1,"cachedCount":0},"timestamp":1769993206421}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":2.1970239764015806,"tier":2,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":111,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993206451}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":2,"cachedCount":0},"timestamp":1769993206451}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":2.416726374041739,"tier":2,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":111,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993206906}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":2,"cachedCount":0},"timestamp":1769993206906}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":2.6583990114459133,"tier":2,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":111,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993206933}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":2,"cachedCount":0},"timestamp":1769993206934}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":2.924238912590505,"tier":2,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":111,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993206951}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":2,"cachedCount":0},"timestamp":1769993206951}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":3.2166628038495557,"tier":2,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":111,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993207050}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":2,"cachedCount":0},"timestamp":1769993207050}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":3.5383290842345114,"tier":2,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":111,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993207547}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":2,"cachedCount":0},"timestamp":1769993207547}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":3.8921619926579627,"tier":2,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":111,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993207584}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":2,"cachedCount":0},"timestamp":1769993207585}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":4.28137819192376,"tier":2,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":111,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993207656}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":2,"cachedCount":0},"timestamp":1769993207656}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":4.709516011116136,"tier":2,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":111,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993207674}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":2,"cachedCount":0},"timestamp":1769993207674}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":5.18046761222775,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993207720}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993207720}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":4.709516011116136,"tier":2,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":111,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993208624}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":2,"cachedCount":0},"timestamp":1769993208624}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":3.892161992657963,"tier":2,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":111,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993208650}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":2,"cachedCount":0},"timestamp":1769993208650}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":3.538329084234512,"tier":2,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":111,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993208686}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":2,"cachedCount":0},"timestamp":1769993208686}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":3.8921619926579636,"tier":2,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":111,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993209088}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":2,"cachedCount":0},"timestamp":1769993209088}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":4.281378191923761,"tier":2,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":111,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993209111}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":2,"cachedCount":0},"timestamp":1769993209111}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":4.709516011116137,"tier":2,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":111,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993209130}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":2,"cachedCount":0},"timestamp":1769993209131}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":5.180467612227751,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993209150}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993209150}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":5.698514373450527,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993209173}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993209173}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":6.26836581079558,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993209194}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993209194}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":6.895202391875139,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993209253}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993209254}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":7.584722631062653,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993209506}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993209507}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":8.343194894168919,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993209531}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993209532}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":9.177514383585812,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993209585}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993209585}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":11.104792404138836,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993209652}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993209652}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":12.21527164455272,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993209826}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993209826}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":14.780478689908794,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993209872}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993209872}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":16.258526558899675,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993210108}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993210109}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":17.884379214789643,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993210154}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993210154}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":19.67281713626861,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993210196}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993210197}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":21.640098849895473,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993210407}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993210408}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":26.184519608373527,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993210454}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993210455}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":31.683268726131974,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993210483}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993210484}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":34.85159559874518,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993210738}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993210739}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":42.170430674481665,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993210799}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993210800}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":46.38747374192984,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993210979}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993210980}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":51.02622111612283,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993211007}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993211008}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":61.74172755050863,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993211035}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993211036}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":67.91590030555949,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993211282}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993211282}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":74.70749033611544,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993211312}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993211312}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":90.3960633066997,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993211340}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993211340}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":99.43566963736968,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993211581}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993211581}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":100,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993211612}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993211612}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":90.9090909090909,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993212736}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993212736}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":75.13148009015777,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993212765}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993212765}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":68.30134553650706,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993212793}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993212794}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":56.447393005377734,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993212824}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993212824}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":51.31581182307067,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993213111}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993213111}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":46.650738020973336,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993213138}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993213139}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":38.55432894295317,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993213167}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993213167}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":35.04938994813924,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993213218}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993213218}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":31.863081771035674,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993213249}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993213249}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":28.966437973668793,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993213518}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993213518}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":23.939204936916358,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993213576}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993213577}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":21.76291357901487,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993213860}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993213860}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":19.78446689001352,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993213891}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993213891}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":17.985878990921382,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993213918}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993213918}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":16.3507990826558,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993213945}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993213946}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":14.864362802414364,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993213974}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993213974}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":13.513057093103967,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993214246}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993214246}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":11.167815779424766,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993214273}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993214273}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":10.152559799477059,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993214308}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993214308}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":9.229599817706417,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993214325}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993214326}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993214629}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":8.390545288824015,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993214629}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":6.934334949441334,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993214674}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993214674}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":6.303940863128485,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993214699}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993214699}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":5.730855330116804,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993214714}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993214714}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":5.209868481924367,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993214983}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993214983}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":4.736244074476697,"tier":2,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":111,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993215006}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":2,"cachedCount":0},"timestamp":1769993215006}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":4.305676431342452,"tier":2,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":111,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993215021}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":2,"cachedCount":0},"timestamp":1769993215022}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":3.9142513012204105,"tier":2,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":111,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993215049}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":2,"cachedCount":0},"timestamp":1769993215050}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":3.5584102738367367,"tier":2,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":111,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993215085}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":2,"cachedCount":0},"timestamp":1769993215085}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":3.2349184307606698,"tier":2,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":111,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993215113}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":2,"cachedCount":0},"timestamp":1769993215113}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":2.940834937055154,"tier":2,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":111,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993215380}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":2,"cachedCount":0},"timestamp":1769993215380}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":2.673486306413776,"tier":2,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":111,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993215398}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":2,"cachedCount":0},"timestamp":1769993215398}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":2.4304420967397964,"tier":2,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":111,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993215430}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":2,"cachedCount":0},"timestamp":1769993215430}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":2.2094928152179967,"tier":2,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":111,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993215451}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":2,"cachedCount":0},"timestamp":1769993215452}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":2.0086298320163607,"tier":2,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":111,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993215480}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":2,"cachedCount":0},"timestamp":1769993215480}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":1.8260271200148732,"tier":1,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":56,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993215789}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":1,"cachedCount":0},"timestamp":1769993215789}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":1.6600246545589756,"tier":1,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":56,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993215811}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":1,"cachedCount":0},"timestamp":1769993215811}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":1.5091133223263415,"tier":1,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":56,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993215833}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":1,"cachedCount":0},"timestamp":1769993215833}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":1.371921202114856,"tier":1,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":56,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993215856}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":1,"cachedCount":0},"timestamp":1769993215857}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":1.2472010928316872,"tier":1,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":56,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993215877}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":1,"cachedCount":0},"timestamp":1769993215877}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":1.1338191753015339,"tier":1,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":56,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993216180}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":1,"cachedCount":0},"timestamp":1769993216181}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":1.0307447048195761,"tier":1,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":56,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993216198}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":1,"cachedCount":0},"timestamp":1769993216198}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":0.9370406407450692,"tier":1,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":56,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993216216}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":1,"cachedCount":0},"timestamp":1769993216217}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":0.8518551279500629,"tier":1,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":56,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993216231}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":1,"cachedCount":0},"timestamp":1769993216231}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":12.6,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993217645}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993217645}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":13.6,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993217688}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993217688}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":14.7,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993218766}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993218766}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":15.7,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993218823}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993218823}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":16.7,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993218888}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993218888}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":17.8,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993219008}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993219008}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":18.8,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993219046}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993219047}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":19.9,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993219086}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993219086}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":20.9,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993219193}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993219193}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":21.9,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993219260}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993219260}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":23,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993219380}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993219381}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":24,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993219461}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993219461}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":25.1,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993219489}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993219489}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":26.1,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993219558}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993219558}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":29.2,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993219597}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993219597}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":31.3,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993219629}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993219630}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":32.3,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993219657}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993219657}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":33.4,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993219685}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993219685}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":34.4,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993219712}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993219712}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":35.5,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993219827}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993219827}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":37.5,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993219948}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993219948}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":40.7,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993219977}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993219977}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":42.7,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993220005}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993220006}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":43.8,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993220037}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993220038}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":45.9,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993220066}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993220067}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":52.1,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993220094}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993220094}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":56.3,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993220122}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993220122}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":63.6,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993220152}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993220153}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":65.6,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993220180}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993220180}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":67.7,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993220207}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993220207}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":68.8,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993220235}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993220235}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":70.8,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993220279}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993220279}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":72.9,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993220392}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993220393}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":74,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993220427}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993220428}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":76,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993220454}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993220454}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":77.1,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993220493}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993220494}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":78.1,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993220559}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993220559}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":79.2,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993220618}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993220618}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":81.2,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993220652}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993220653}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":82.3,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993220680}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993220681}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":83.3,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993220708}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993220708}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":85.4,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993220737}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993220737}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":86.4,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993220766}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993220766}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":87.5,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993220793}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993220794}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":89.6,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993220876}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993220877}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":90.6,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993220904}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993220904}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":91.6,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993220930}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993220931}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":92.7,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993220957}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993220958}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":93.7,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993220994}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993220994}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":95.8,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993221021}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993221022}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":96.8,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993221049}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993221049}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":98.9,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993221077}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993221077}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":100,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993221107}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993221107}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":98.9,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993221749}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993221750}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":96.8,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993221788}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993221788}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":95.8,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993221814}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993221815}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":94.8,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993221845}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993221846}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":93.7,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993221893}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993221893}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":92.7,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993221919}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993221920}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":91.6,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993222009}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993222010}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":90.6,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993222060}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993222062}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":88.5,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993222145}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993222145}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":87.5,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993222183}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993222184}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":85.4,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993222222}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993222222}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":84.4,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993222250}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993222250}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":83.3,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993222277}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993222277}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":82.3,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993222304}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993222305}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":80.2,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993222333}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993222333}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":77.1,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993222362}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993222363}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":74,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993222390}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993222390}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":71.9,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993222419}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993222419}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":70.8,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993222446}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993222446}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":68.8,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993222475}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993222475}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":65.6,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993222503}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993222503}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":60.4,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993222531}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993222532}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":57.3,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993222559}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993222559}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":54.2,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993222590}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993222590}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":51.1,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993222617}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993222618}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":45.9,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993222645}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993222645}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":41.7,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993222673}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993222674}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":39.6,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993222703}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993222704}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":37.5,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993222772}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993222772}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":34.4,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993222832}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993222833}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":28.2,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993222866}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993222866}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":27.1,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993222894}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993222894}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":26.1,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993222921}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993222922}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":25.1,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993222949}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993222950}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":23,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993222999}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993223000}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":18.8,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993223028}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993223028}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":16.7,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993223057}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993223057}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":14.7,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993223085}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993223085}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":13.6,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993223130}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993223130}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":12.6,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993223159}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993223159}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":11.5,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993223188}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993223188}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":10.5,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993223223}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993223223}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":9.5,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993223241}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993223241}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":8.4,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993223285}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993223285}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":7.4,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993223303}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993223304}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":6.3,"tier":3,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":221,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993223339}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":3,"cachedCount":53},"timestamp":1769993223340}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":4.3,"tier":2,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":111,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993223387}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":2,"cachedCount":0},"timestamp":1769993223388}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":3.2,"tier":2,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":111,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993223417}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":2,"cachedCount":0},"timestamp":1769993223418}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":2.2,"tier":2,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":111,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993223484}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":2,"cachedCount":0},"timestamp":1769993223484}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":1.1,"tier":1,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":56,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993223499}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":1,"cachedCount":0},"timestamp":1769993223499}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"A","location":"use-filmstrip.ts:58","message":"Computed filmstrip timestamps","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","zoomLevel":0.13849909222110923,"tier":0,"duration":55.05451247165533,"visibleStartTime":0,"visibleEndTime":55.05451247165533,"timestampCount":12,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993223560}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"C","location":"use-filmstrip.ts:82","message":"Cached frames loaded","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","tier":0,"cachedCount":0},"timestamp":1769993223561}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H","location":"worker.ts:62","message":"Filmstrip extract request","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","fileType":"","fileSize":3787343,"timestampCount":12,"firstTimestamp":0,"lastTimestamp":55},"timestamp":1769993244027}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"K","location":"worker.ts:104","message":"Requesting sample for timestamp","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","timestamp":0},"timestamp":1769993244030}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"L","location":"worker.ts:132","message":"Sample received","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","timestamp":0},"timestamp":1769993244050}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"J","location":"worker.ts:131","message":"Frame encoded to dataUrl","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","timestamp":0,"dataUrlLength":3067,"dataUrlPrefix":"data:image/jpeg;base64,/9j/4AAQSkZJRgABA"},"timestamp":1769993244064}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"K","location":"worker.ts:104","message":"Requesting sample for timestamp","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","timestamp":55},"timestamp":1769993244403}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"L","location":"worker.ts:132","message":"Sample received","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","timestamp":55},"timestamp":1769993244419}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"J","location":"worker.ts:131","message":"Frame encoded to dataUrl","data":{"mediaId":"32045f82-11be-4928-9542-ec0844f6306a","timestamp":55,"dataUrlLength":1263,"dataUrlPrefix":"data:image/jpeg;base64,/9j/4AAQSkZJRgABA"},"timestamp":1769993244429}
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"plugins": {
|
||||||
|
"figma": {
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,15 @@
|
|||||||
# Contributing to OpenCut
|
# Contributing to OpenCut
|
||||||
|
|
||||||
|
⚠️ We are currently NOT accepting feature PRs while we build out the core editor.
|
||||||
|
|
||||||
|
If you want to contribute:
|
||||||
|
|
||||||
|
1. Open an issue first to discuss
|
||||||
|
2. Wait for maintainer approval
|
||||||
|
3. Only then start coding
|
||||||
|
|
||||||
|
Critical bug fixes may be accepted on a case-by-case basis.
|
||||||
|
|
||||||
Thank you for your interest in contributing to OpenCut! This document provides guidelines and instructions for contributing.
|
Thank you for your interest in contributing to OpenCut! This document provides guidelines and instructions for contributing.
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|||||||
@@ -1,50 +1,11 @@
|
|||||||
## Description
|
⚠️ READ BEFORE SUBMITTING ⚠️
|
||||||
|
|
||||||
Please include a summary of the changes and the related issue. Please also include relevant motivation and context.
|
We are not currently accepting PRs except for critical bugs.
|
||||||
|
|
||||||
Fixes # (issue)
|
If this is a bug fix:
|
||||||
|
- [ ] I've opened an issue first
|
||||||
|
- [ ] This was approved by a maintainer
|
||||||
|
|
||||||
## Type of change
|
If this is a feature:
|
||||||
|
|
||||||
Please delete options that are not relevant.
|
This PR will be closed. Please open an issue to discuss first.
|
||||||
|
|
||||||
- [ ] Bug fix (non-breaking change which fixes an issue)
|
|
||||||
- [ ] New feature (non-breaking change which adds functionality)
|
|
||||||
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
|
||||||
- [ ] This change requires a documentation update
|
|
||||||
- [ ] Performance improvement
|
|
||||||
- [ ] Code refactoring
|
|
||||||
- [ ] Tests
|
|
||||||
|
|
||||||
## How Has This Been Tested?
|
|
||||||
|
|
||||||
Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration
|
|
||||||
|
|
||||||
- [ ] Test A
|
|
||||||
- [ ] Test B
|
|
||||||
|
|
||||||
**Test Configuration**:
|
|
||||||
|
|
||||||
- Node version:
|
|
||||||
- Browser (if applicable):
|
|
||||||
- Operating System:
|
|
||||||
|
|
||||||
## Screenshots (if applicable)
|
|
||||||
|
|
||||||
Add screenshots to help explain your changes.
|
|
||||||
|
|
||||||
## Checklist:
|
|
||||||
|
|
||||||
- [ ] My code follows the style guidelines of this project
|
|
||||||
- [ ] I have performed a self-review of my code
|
|
||||||
- [ ] I have added screenshots if ui has been changed
|
|
||||||
- [ ] I have commented my code, particularly in hard-to-understand areas
|
|
||||||
- [ ] I have made corresponding changes to the documentation
|
|
||||||
- [ ] My changes generate no new warnings
|
|
||||||
- [ ] I have added tests that prove my fix is effective or that my feature works
|
|
||||||
- [ ] New and existing unit tests pass locally with my changes
|
|
||||||
- [ ] Any dependent changes have been merged and published in downstream modules
|
|
||||||
|
|
||||||
## Additional context
|
|
||||||
|
|
||||||
Add any other context about the pull request here.
|
|
||||||
@@ -27,6 +27,15 @@ jobs:
|
|||||||
NEXT_PUBLIC_SITE_URL: "http://localhost:3000"
|
NEXT_PUBLIC_SITE_URL: "http://localhost:3000"
|
||||||
UPSTASH_REDIS_REST_URL: "https://your-upstash-redis-url"
|
UPSTASH_REDIS_REST_URL: "https://your-upstash-redis-url"
|
||||||
UPSTASH_REDIS_REST_TOKEN: "your-upstash-redis-token"
|
UPSTASH_REDIS_REST_TOKEN: "your-upstash-redis-token"
|
||||||
|
NEXT_PUBLIC_MARBLE_API_URL: "https://placeholder.example.com"
|
||||||
|
MARBLE_WORKSPACE_KEY: "placeholder"
|
||||||
|
FREESOUND_CLIENT_ID: "placeholder"
|
||||||
|
FREESOUND_API_KEY: "placeholder"
|
||||||
|
CLOUDFLARE_ACCOUNT_ID: "placeholder"
|
||||||
|
R2_ACCESS_KEY_ID: "placeholder"
|
||||||
|
R2_SECRET_ACCESS_KEY: "placeholder"
|
||||||
|
R2_BUCKET_NAME: "placeholder"
|
||||||
|
MODAL_TRANSCRIPTION_URL: "https://placeholder.example.com"
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
{
|
{
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
"[javascript][typescript][javascriptreact][typescriptreact][json][jsonc][css][graphql]": {
|
"[javascript][typescript][javascriptreact][typescriptreact][json][jsonc][css][graphql]": {
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
},
|
},
|
||||||
"typescript.tsdk": "node_modules/typescript/lib",
|
"typescript.tsdk": "node_modules/typescript/lib",
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"editor.formatOnPaste": true,
|
"editor.formatOnPaste": true,
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"source.fixAll.biome": "explicit",
|
"source.fixAll.biome": "explicit",
|
||||||
"source.organizeImports.biome": "explicit"
|
"source.organizeImports.biome": "explicit"
|
||||||
},
|
},
|
||||||
"emmet.showExpandedAbbreviation": "never",
|
"emmet.showExpandedAbbreviation": "never",
|
||||||
"[typescriptreact]": {
|
"[typescriptreact]": {
|
||||||
"editor.defaultFormatter": "biomejs.biome"
|
"editor.defaultFormatter": "biomejs.biome"
|
||||||
},
|
},
|
||||||
"[typescript]": {
|
"[typescript]": {
|
||||||
"editor.defaultFormatter": "biomejs.biome"
|
"editor.defaultFormatter": "biomejs.biome"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,3 +6,5 @@
|
|||||||
!.env.example
|
!.env.example
|
||||||
|
|
||||||
.next/
|
.next/
|
||||||
|
|
||||||
|
.font-cache/
|
||||||
@@ -1,21 +1,21 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://ui.shadcn.com/schema.json",
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
"style": "new-york",
|
"style": "new-york",
|
||||||
"rsc": true,
|
"rsc": true,
|
||||||
"tsx": true,
|
"tsx": true,
|
||||||
"tailwind": {
|
"tailwind": {
|
||||||
"config": "",
|
"config": "",
|
||||||
"css": "src/app/globals.css",
|
"css": "src/app/globals.css",
|
||||||
"baseColor": "neutral",
|
"baseColor": "neutral",
|
||||||
"cssVariables": true,
|
"cssVariables": true,
|
||||||
"prefix": ""
|
"prefix": ""
|
||||||
},
|
},
|
||||||
"aliases": {
|
"aliases": {
|
||||||
"components": "@/components",
|
"components": "@/components",
|
||||||
"utils": "@/lib/utils",
|
"utils": "@/lib/utils",
|
||||||
"ui": "@/components/ui",
|
"ui": "@/components/ui",
|
||||||
"lib": "@/lib",
|
"lib": "@/lib",
|
||||||
"hooks": "@/hooks"
|
"hooks": "@/hooks"
|
||||||
},
|
},
|
||||||
"iconLibrary": "lucide"
|
"iconLibrary": "lucide"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,20 +4,20 @@ import { webEnv } from "@opencut/env/web";
|
|||||||
|
|
||||||
// Load the right env file based on environment
|
// Load the right env file based on environment
|
||||||
if (webEnv.NODE_ENV === "production") {
|
if (webEnv.NODE_ENV === "production") {
|
||||||
dotenv.config({ path: ".env.production" });
|
dotenv.config({ path: ".env.production" });
|
||||||
} else {
|
} else {
|
||||||
dotenv.config({ path: ".env.local" });
|
dotenv.config({ path: ".env.local" });
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
schema: "./src/schema.ts",
|
schema: "./src/schema.ts",
|
||||||
dialect: "postgresql",
|
dialect: "postgresql",
|
||||||
migrations: {
|
migrations: {
|
||||||
table: "drizzle_migrations",
|
table: "drizzle_migrations",
|
||||||
},
|
},
|
||||||
dbCredentials: {
|
dbCredentials: {
|
||||||
url: webEnv.DATABASE_URL,
|
url: webEnv.DATABASE_URL,
|
||||||
},
|
},
|
||||||
out: "./migrations",
|
out: "./migrations",
|
||||||
strict: webEnv.NODE_ENV === "production",
|
strict: webEnv.NODE_ENV === "production",
|
||||||
} satisfies Config;
|
} satisfies Config;
|
||||||
|
|||||||
@@ -1,344 +1,344 @@
|
|||||||
{
|
{
|
||||||
"id": "33a6742f-89da-4ac5-958f-421aa1cf9bd6",
|
"id": "33a6742f-89da-4ac5-958f-421aa1cf9bd6",
|
||||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"dialect": "postgresql",
|
"dialect": "postgresql",
|
||||||
"tables": {
|
"tables": {
|
||||||
"public.accounts": {
|
"public.accounts": {
|
||||||
"name": "accounts",
|
"name": "accounts",
|
||||||
"schema": "",
|
"schema": "",
|
||||||
"columns": {
|
"columns": {
|
||||||
"id": {
|
"id": {
|
||||||
"name": "id",
|
"name": "id",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"primaryKey": true,
|
"primaryKey": true,
|
||||||
"notNull": true
|
"notNull": true
|
||||||
},
|
},
|
||||||
"account_id": {
|
"account_id": {
|
||||||
"name": "account_id",
|
"name": "account_id",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": true
|
"notNull": true
|
||||||
},
|
},
|
||||||
"provider_id": {
|
"provider_id": {
|
||||||
"name": "provider_id",
|
"name": "provider_id",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": true
|
"notNull": true
|
||||||
},
|
},
|
||||||
"user_id": {
|
"user_id": {
|
||||||
"name": "user_id",
|
"name": "user_id",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": true
|
"notNull": true
|
||||||
},
|
},
|
||||||
"access_token": {
|
"access_token": {
|
||||||
"name": "access_token",
|
"name": "access_token",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": false
|
"notNull": false
|
||||||
},
|
},
|
||||||
"refresh_token": {
|
"refresh_token": {
|
||||||
"name": "refresh_token",
|
"name": "refresh_token",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": false
|
"notNull": false
|
||||||
},
|
},
|
||||||
"id_token": {
|
"id_token": {
|
||||||
"name": "id_token",
|
"name": "id_token",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": false
|
"notNull": false
|
||||||
},
|
},
|
||||||
"access_token_expires_at": {
|
"access_token_expires_at": {
|
||||||
"name": "access_token_expires_at",
|
"name": "access_token_expires_at",
|
||||||
"type": "timestamp",
|
"type": "timestamp",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": false
|
"notNull": false
|
||||||
},
|
},
|
||||||
"refresh_token_expires_at": {
|
"refresh_token_expires_at": {
|
||||||
"name": "refresh_token_expires_at",
|
"name": "refresh_token_expires_at",
|
||||||
"type": "timestamp",
|
"type": "timestamp",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": false
|
"notNull": false
|
||||||
},
|
},
|
||||||
"scope": {
|
"scope": {
|
||||||
"name": "scope",
|
"name": "scope",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": false
|
"notNull": false
|
||||||
},
|
},
|
||||||
"password": {
|
"password": {
|
||||||
"name": "password",
|
"name": "password",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": false
|
"notNull": false
|
||||||
},
|
},
|
||||||
"created_at": {
|
"created_at": {
|
||||||
"name": "created_at",
|
"name": "created_at",
|
||||||
"type": "timestamp",
|
"type": "timestamp",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": true
|
"notNull": true
|
||||||
},
|
},
|
||||||
"updated_at": {
|
"updated_at": {
|
||||||
"name": "updated_at",
|
"name": "updated_at",
|
||||||
"type": "timestamp",
|
"type": "timestamp",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": true
|
"notNull": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"indexes": {},
|
"indexes": {},
|
||||||
"foreignKeys": {
|
"foreignKeys": {
|
||||||
"accounts_user_id_users_id_fk": {
|
"accounts_user_id_users_id_fk": {
|
||||||
"name": "accounts_user_id_users_id_fk",
|
"name": "accounts_user_id_users_id_fk",
|
||||||
"tableFrom": "accounts",
|
"tableFrom": "accounts",
|
||||||
"tableTo": "users",
|
"tableTo": "users",
|
||||||
"columnsFrom": ["user_id"],
|
"columnsFrom": ["user_id"],
|
||||||
"columnsTo": ["id"],
|
"columnsTo": ["id"],
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"compositePrimaryKeys": {},
|
"compositePrimaryKeys": {},
|
||||||
"uniqueConstraints": {},
|
"uniqueConstraints": {},
|
||||||
"policies": {},
|
"policies": {},
|
||||||
"checkConstraints": {},
|
"checkConstraints": {},
|
||||||
"isRLSEnabled": true
|
"isRLSEnabled": true
|
||||||
},
|
},
|
||||||
"public.sessions": {
|
"public.sessions": {
|
||||||
"name": "sessions",
|
"name": "sessions",
|
||||||
"schema": "",
|
"schema": "",
|
||||||
"columns": {
|
"columns": {
|
||||||
"id": {
|
"id": {
|
||||||
"name": "id",
|
"name": "id",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"primaryKey": true,
|
"primaryKey": true,
|
||||||
"notNull": true
|
"notNull": true
|
||||||
},
|
},
|
||||||
"expires_at": {
|
"expires_at": {
|
||||||
"name": "expires_at",
|
"name": "expires_at",
|
||||||
"type": "timestamp",
|
"type": "timestamp",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": true
|
"notNull": true
|
||||||
},
|
},
|
||||||
"token": {
|
"token": {
|
||||||
"name": "token",
|
"name": "token",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": true
|
"notNull": true
|
||||||
},
|
},
|
||||||
"created_at": {
|
"created_at": {
|
||||||
"name": "created_at",
|
"name": "created_at",
|
||||||
"type": "timestamp",
|
"type": "timestamp",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": true
|
"notNull": true
|
||||||
},
|
},
|
||||||
"updated_at": {
|
"updated_at": {
|
||||||
"name": "updated_at",
|
"name": "updated_at",
|
||||||
"type": "timestamp",
|
"type": "timestamp",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": true
|
"notNull": true
|
||||||
},
|
},
|
||||||
"ip_address": {
|
"ip_address": {
|
||||||
"name": "ip_address",
|
"name": "ip_address",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": false
|
"notNull": false
|
||||||
},
|
},
|
||||||
"user_agent": {
|
"user_agent": {
|
||||||
"name": "user_agent",
|
"name": "user_agent",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": false
|
"notNull": false
|
||||||
},
|
},
|
||||||
"user_id": {
|
"user_id": {
|
||||||
"name": "user_id",
|
"name": "user_id",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": true
|
"notNull": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"indexes": {},
|
"indexes": {},
|
||||||
"foreignKeys": {
|
"foreignKeys": {
|
||||||
"sessions_user_id_users_id_fk": {
|
"sessions_user_id_users_id_fk": {
|
||||||
"name": "sessions_user_id_users_id_fk",
|
"name": "sessions_user_id_users_id_fk",
|
||||||
"tableFrom": "sessions",
|
"tableFrom": "sessions",
|
||||||
"tableTo": "users",
|
"tableTo": "users",
|
||||||
"columnsFrom": ["user_id"],
|
"columnsFrom": ["user_id"],
|
||||||
"columnsTo": ["id"],
|
"columnsTo": ["id"],
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"compositePrimaryKeys": {},
|
"compositePrimaryKeys": {},
|
||||||
"uniqueConstraints": {
|
"uniqueConstraints": {
|
||||||
"sessions_token_unique": {
|
"sessions_token_unique": {
|
||||||
"name": "sessions_token_unique",
|
"name": "sessions_token_unique",
|
||||||
"nullsNotDistinct": false,
|
"nullsNotDistinct": false,
|
||||||
"columns": ["token"]
|
"columns": ["token"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"policies": {},
|
"policies": {},
|
||||||
"checkConstraints": {},
|
"checkConstraints": {},
|
||||||
"isRLSEnabled": true
|
"isRLSEnabled": true
|
||||||
},
|
},
|
||||||
"public.users": {
|
"public.users": {
|
||||||
"name": "users",
|
"name": "users",
|
||||||
"schema": "",
|
"schema": "",
|
||||||
"columns": {
|
"columns": {
|
||||||
"id": {
|
"id": {
|
||||||
"name": "id",
|
"name": "id",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"primaryKey": true,
|
"primaryKey": true,
|
||||||
"notNull": true
|
"notNull": true
|
||||||
},
|
},
|
||||||
"name": {
|
"name": {
|
||||||
"name": "name",
|
"name": "name",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": true
|
"notNull": true
|
||||||
},
|
},
|
||||||
"email": {
|
"email": {
|
||||||
"name": "email",
|
"name": "email",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": true
|
"notNull": true
|
||||||
},
|
},
|
||||||
"email_verified": {
|
"email_verified": {
|
||||||
"name": "email_verified",
|
"name": "email_verified",
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": true
|
"notNull": true
|
||||||
},
|
},
|
||||||
"image": {
|
"image": {
|
||||||
"name": "image",
|
"name": "image",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": false
|
"notNull": false
|
||||||
},
|
},
|
||||||
"created_at": {
|
"created_at": {
|
||||||
"name": "created_at",
|
"name": "created_at",
|
||||||
"type": "timestamp",
|
"type": "timestamp",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": true
|
"notNull": true
|
||||||
},
|
},
|
||||||
"updated_at": {
|
"updated_at": {
|
||||||
"name": "updated_at",
|
"name": "updated_at",
|
||||||
"type": "timestamp",
|
"type": "timestamp",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": true
|
"notNull": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"indexes": {},
|
"indexes": {},
|
||||||
"foreignKeys": {},
|
"foreignKeys": {},
|
||||||
"compositePrimaryKeys": {},
|
"compositePrimaryKeys": {},
|
||||||
"uniqueConstraints": {
|
"uniqueConstraints": {
|
||||||
"users_email_unique": {
|
"users_email_unique": {
|
||||||
"name": "users_email_unique",
|
"name": "users_email_unique",
|
||||||
"nullsNotDistinct": false,
|
"nullsNotDistinct": false,
|
||||||
"columns": ["email"]
|
"columns": ["email"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"policies": {},
|
"policies": {},
|
||||||
"checkConstraints": {},
|
"checkConstraints": {},
|
||||||
"isRLSEnabled": true
|
"isRLSEnabled": true
|
||||||
},
|
},
|
||||||
"public.verifications": {
|
"public.verifications": {
|
||||||
"name": "verifications",
|
"name": "verifications",
|
||||||
"schema": "",
|
"schema": "",
|
||||||
"columns": {
|
"columns": {
|
||||||
"id": {
|
"id": {
|
||||||
"name": "id",
|
"name": "id",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"primaryKey": true,
|
"primaryKey": true,
|
||||||
"notNull": true
|
"notNull": true
|
||||||
},
|
},
|
||||||
"identifier": {
|
"identifier": {
|
||||||
"name": "identifier",
|
"name": "identifier",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": true
|
"notNull": true
|
||||||
},
|
},
|
||||||
"value": {
|
"value": {
|
||||||
"name": "value",
|
"name": "value",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": true
|
"notNull": true
|
||||||
},
|
},
|
||||||
"expires_at": {
|
"expires_at": {
|
||||||
"name": "expires_at",
|
"name": "expires_at",
|
||||||
"type": "timestamp",
|
"type": "timestamp",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": true
|
"notNull": true
|
||||||
},
|
},
|
||||||
"created_at": {
|
"created_at": {
|
||||||
"name": "created_at",
|
"name": "created_at",
|
||||||
"type": "timestamp",
|
"type": "timestamp",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": false
|
"notNull": false
|
||||||
},
|
},
|
||||||
"updated_at": {
|
"updated_at": {
|
||||||
"name": "updated_at",
|
"name": "updated_at",
|
||||||
"type": "timestamp",
|
"type": "timestamp",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": false
|
"notNull": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"indexes": {},
|
"indexes": {},
|
||||||
"foreignKeys": {},
|
"foreignKeys": {},
|
||||||
"compositePrimaryKeys": {},
|
"compositePrimaryKeys": {},
|
||||||
"uniqueConstraints": {},
|
"uniqueConstraints": {},
|
||||||
"policies": {},
|
"policies": {},
|
||||||
"checkConstraints": {},
|
"checkConstraints": {},
|
||||||
"isRLSEnabled": true
|
"isRLSEnabled": true
|
||||||
},
|
},
|
||||||
"public.waitlist": {
|
"public.waitlist": {
|
||||||
"name": "waitlist",
|
"name": "waitlist",
|
||||||
"schema": "",
|
"schema": "",
|
||||||
"columns": {
|
"columns": {
|
||||||
"id": {
|
"id": {
|
||||||
"name": "id",
|
"name": "id",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"primaryKey": true,
|
"primaryKey": true,
|
||||||
"notNull": true
|
"notNull": true
|
||||||
},
|
},
|
||||||
"email": {
|
"email": {
|
||||||
"name": "email",
|
"name": "email",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": true
|
"notNull": true
|
||||||
},
|
},
|
||||||
"created_at": {
|
"created_at": {
|
||||||
"name": "created_at",
|
"name": "created_at",
|
||||||
"type": "timestamp",
|
"type": "timestamp",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": true
|
"notNull": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"indexes": {},
|
"indexes": {},
|
||||||
"foreignKeys": {},
|
"foreignKeys": {},
|
||||||
"compositePrimaryKeys": {},
|
"compositePrimaryKeys": {},
|
||||||
"uniqueConstraints": {
|
"uniqueConstraints": {
|
||||||
"waitlist_email_unique": {
|
"waitlist_email_unique": {
|
||||||
"name": "waitlist_email_unique",
|
"name": "waitlist_email_unique",
|
||||||
"nullsNotDistinct": false,
|
"nullsNotDistinct": false,
|
||||||
"columns": ["email"]
|
"columns": ["email"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"policies": {},
|
"policies": {},
|
||||||
"checkConstraints": {},
|
"checkConstraints": {},
|
||||||
"isRLSEnabled": true
|
"isRLSEnabled": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"enums": {},
|
"enums": {},
|
||||||
"schemas": {},
|
"schemas": {},
|
||||||
"sequences": {},
|
"sequences": {},
|
||||||
"roles": {},
|
"roles": {},
|
||||||
"policies": {},
|
"policies": {},
|
||||||
"views": {},
|
"views": {},
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"columns": {},
|
"columns": {},
|
||||||
"schemas": {},
|
"schemas": {},
|
||||||
"tables": {}
|
"tables": {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"dialect": "postgresql",
|
"dialect": "postgresql",
|
||||||
"entries": [
|
"entries": [
|
||||||
{
|
{
|
||||||
"idx": 0,
|
"idx": 0,
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"when": 1750753385927,
|
"when": 1750753385927,
|
||||||
"tag": "0000_brainy_saracen",
|
"tag": "0000_brainy_saracen",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,48 +2,48 @@ import type { NextConfig } from "next";
|
|||||||
import { withBotId } from "botid/next/config";
|
import { withBotId } from "botid/next/config";
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
compiler: {
|
compiler: {
|
||||||
removeConsole: process.env.NODE_ENV === "production",
|
removeConsole: process.env.NODE_ENV === "production",
|
||||||
},
|
},
|
||||||
reactStrictMode: true,
|
reactStrictMode: true,
|
||||||
productionBrowserSourceMaps: true,
|
productionBrowserSourceMaps: true,
|
||||||
output: "standalone",
|
output: "standalone",
|
||||||
images: {
|
images: {
|
||||||
remotePatterns: [
|
remotePatterns: [
|
||||||
{
|
{
|
||||||
protocol: "https",
|
protocol: "https",
|
||||||
hostname: "plus.unsplash.com",
|
hostname: "plus.unsplash.com",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
protocol: "https",
|
protocol: "https",
|
||||||
hostname: "images.unsplash.com",
|
hostname: "images.unsplash.com",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
protocol: "https",
|
protocol: "https",
|
||||||
hostname: "images.marblecms.com",
|
hostname: "images.marblecms.com",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
protocol: "https",
|
protocol: "https",
|
||||||
hostname: "lh3.googleusercontent.com",
|
hostname: "lh3.googleusercontent.com",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
protocol: "https",
|
protocol: "https",
|
||||||
hostname: "avatars.githubusercontent.com",
|
hostname: "avatars.githubusercontent.com",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
protocol: "https",
|
protocol: "https",
|
||||||
hostname: "api.iconify.design",
|
hostname: "api.iconify.design",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
protocol: "https",
|
protocol: "https",
|
||||||
hostname: "api.simplesvg.com",
|
hostname: "api.simplesvg.com",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
protocol: "https",
|
protocol: "https",
|
||||||
hostname: "api.unisvg.com",
|
hostname: "api.unisvg.com",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withBotId(nextConfig);
|
export default withBotId(nextConfig);
|
||||||
|
|||||||
@@ -1,98 +1,107 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencut/web",
|
"name": "@opencut/web",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"packageManager": "bun@1.2.18",
|
"packageManager": "bun@1.2.18",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev --turbopack",
|
"dev": "next dev --turbopack",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "biome check src/",
|
"lint": "biome check src/",
|
||||||
"lint:fix": "biome check src/ --write",
|
"lint:fix": "biome check src/ --write",
|
||||||
"format": "biome format src/ --write",
|
"format": "biome format src/ --write",
|
||||||
"db:generate": "drizzle-kit generate",
|
"db:generate": "drizzle-kit generate",
|
||||||
"db:migrate": "drizzle-kit migrate",
|
"db:migrate": "drizzle-kit migrate",
|
||||||
"db:push:local": "cross-env NODE_ENV=development drizzle-kit push",
|
"db:push:local": "cross-env NODE_ENV=development drizzle-kit push",
|
||||||
"db:push:prod": "cross-env NODE_ENV=production drizzle-kit push"
|
"db:push:prod": "cross-env NODE_ENV=production drizzle-kit push"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ffmpeg/core": "^0.12.10",
|
"@ffmpeg/core": "^0.12.10",
|
||||||
"@ffmpeg/ffmpeg": "^0.12.15",
|
"@ffmpeg/ffmpeg": "^0.12.15",
|
||||||
"@ffmpeg/util": "^0.12.2",
|
"@ffmpeg/util": "^0.12.2",
|
||||||
"@hello-pangea/dnd": "^18.0.1",
|
"@hello-pangea/dnd": "^18.0.1",
|
||||||
"@hookform/resolvers": "^3.9.1",
|
"@hookform/resolvers": "^3.9.1",
|
||||||
"@hugeicons/core-free-icons": "^3.1.1",
|
"@hugeicons/core-free-icons": "^3.1.1",
|
||||||
"@hugeicons/react": "^1.1.4",
|
"@hugeicons/react": "^1.1.4",
|
||||||
"@huggingface/transformers": "^3.8.1",
|
"@huggingface/transformers": "^3.8.1",
|
||||||
"@opencut/env": "workspace:*",
|
"@opencut/env": "workspace:*",
|
||||||
"@opencut/ui": "workspace:*",
|
"@opencut/ui": "workspace:*",
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
"@radix-ui/react-accordion": "^1.2.12",
|
||||||
"@radix-ui/react-select": "^2.2.6",
|
"@radix-ui/react-checkbox": "^1.3.3",
|
||||||
"@radix-ui/react-separator": "^1.1.8",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
"@radix-ui/react-primitive": "^2.1.4",
|
||||||
"@upstash/ratelimit": "^2.0.6",
|
"@radix-ui/react-select": "^2.2.6",
|
||||||
"@upstash/redis": "^1.35.4",
|
"@radix-ui/react-separator": "^1.1.8",
|
||||||
"@vercel/analytics": "^1.4.1",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
"aws4fetch": "^1.0.20",
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
"better-auth": "^1.2.7",
|
"@types/culori": "^4.0.1",
|
||||||
"botid": "^1.4.2",
|
"@upstash/ratelimit": "^2.0.6",
|
||||||
"class-variance-authority": "^0.7.1",
|
"@upstash/redis": "^1.35.4",
|
||||||
"clsx": "^2.1.1",
|
"@vercel/analytics": "^1.4.1",
|
||||||
"cmdk": "^1.0.0",
|
"aws4fetch": "^1.0.20",
|
||||||
"dayjs": "^1.11.13",
|
"better-auth": "^1.2.7",
|
||||||
"drizzle-orm": "^0.44.2",
|
"botid": "^1.4.2",
|
||||||
"embla-carousel-react": "^8.5.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"eventemitter3": "^5.0.1",
|
"clsx": "^2.1.1",
|
||||||
"feed": "^5.1.0",
|
"cmdk": "^1.0.0",
|
||||||
"input-otp": "^1.4.1",
|
"culori": "^4.0.2",
|
||||||
"lucide-react": "^0.562.0",
|
"dayjs": "^1.11.13",
|
||||||
"mediabunny": "^1.29.1",
|
"drizzle-orm": "^0.44.2",
|
||||||
"motion": "^12.18.1",
|
"embla-carousel-react": "^8.5.1",
|
||||||
"nanoid": "^5.1.5",
|
"eventemitter3": "^5.0.1",
|
||||||
"next": "16.1.3",
|
"feed": "^5.1.0",
|
||||||
"next-themes": "^0.4.4",
|
"input-otp": "^1.4.1",
|
||||||
"pg": "^8.16.2",
|
"lucide-react": "^0.562.0",
|
||||||
"postgres": "^3.4.5",
|
"mediabunny": "^1.29.1",
|
||||||
"radix-ui": "^1.4.2",
|
"motion": "^12.18.1",
|
||||||
"react": "^19.0.0",
|
"nanoid": "^5.1.5",
|
||||||
"react-day-picker": "^8.10.1",
|
"next": "16.1.3",
|
||||||
"react-dom": "^19.0.0",
|
"next-themes": "^0.4.4",
|
||||||
"react-hook-form": "^7.54.0",
|
"pg": "^8.16.2",
|
||||||
"react-icons": "^5.4.0",
|
"postgres": "^3.4.5",
|
||||||
"react-markdown": "^10.1.0",
|
"radix-ui": "^1.4.3",
|
||||||
"react-phone-number-input": "^3.4.11",
|
"react": "^19.0.0",
|
||||||
"react-resizable-panels": "^2.1.7",
|
"react-day-picker": "^8.10.1",
|
||||||
"recharts": "^2.14.1",
|
"react-dom": "^19.0.0",
|
||||||
"rehype-autolink-headings": "^7.1.0",
|
"react-hook-form": "^7.54.0",
|
||||||
"rehype-parse": "^9.0.1",
|
"react-icons": "^5.4.0",
|
||||||
"rehype-sanitize": "^6.0.0",
|
"react-markdown": "^10.1.0",
|
||||||
"rehype-slug": "^6.0.0",
|
"react-phone-number-input": "^3.4.11",
|
||||||
"rehype-stringify": "^10.0.1",
|
"react-resizable-panels": "^2.1.7",
|
||||||
"sonner": "^1.7.1",
|
"react-window": "^2.2.7",
|
||||||
"tailwind-merge": "^2.5.5",
|
"recharts": "^2.14.1",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"rehype-autolink-headings": "^7.1.0",
|
||||||
"unified": "^11.0.5",
|
"rehype-parse": "^9.0.1",
|
||||||
"use-deep-compare-effect": "^1.8.1",
|
"rehype-sanitize": "^6.0.0",
|
||||||
"vaul": "^1.1.1",
|
"rehype-slug": "^6.0.0",
|
||||||
"wavesurfer.js": "^7.9.8",
|
"rehype-stringify": "^10.0.1",
|
||||||
"zod": "^3.25.67",
|
"sonner": "^1.7.1",
|
||||||
"zustand": "^5.0.2"
|
"tailwind-merge": "^2.5.5",
|
||||||
},
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"devDependencies": {
|
"unified": "^11.0.5",
|
||||||
"@tailwindcss/postcss": "^4.1.11",
|
"use-deep-compare-effect": "^1.8.1",
|
||||||
"@tailwindcss/typography": "^0.5.16",
|
"vaul": "^1.1.2",
|
||||||
"@types/bun": "latest",
|
"wavesurfer.js": "^7.9.8",
|
||||||
"@types/node": "^24.2.1",
|
"zod": "^3.25.67",
|
||||||
"@types/pg": "^8.15.4",
|
"zustand": "^5.0.2"
|
||||||
"@types/react": "^19",
|
},
|
||||||
"@types/react-dom": "^19",
|
"devDependencies": {
|
||||||
"cross-env": "^7.0.3",
|
"@napi-rs/canvas": "^0.1.92",
|
||||||
"drizzle-kit": "^0.31.4",
|
"@tailwindcss/postcss": "^4.1.11",
|
||||||
"dotenv": "^16.5.0",
|
"@tailwindcss/typography": "^0.5.16",
|
||||||
"postcss": "^8",
|
"@types/bun": "latest",
|
||||||
"tailwindcss": "^4.1.11",
|
"@types/node": "^24.2.1",
|
||||||
"tsx": "^4.7.1",
|
"@types/pg": "^8.15.4",
|
||||||
"typescript": "^5.8.3"
|
"@types/react": "^19",
|
||||||
}
|
"@types/react-dom": "^19",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
|
"dotenv": "^16.5.0",
|
||||||
|
"drizzle-kit": "^0.31.4",
|
||||||
|
"postcss": "^8",
|
||||||
|
"sharp": "^0.34.5",
|
||||||
|
"tailwindcss": "^4.1.11",
|
||||||
|
"tsx": "^4.7.1",
|
||||||
|
"typescript": "^5.8.3"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
/** @type {import('postcss-load-config').Config} */
|
/** @type {import('postcss-load-config').Config} */
|
||||||
const config = {
|
const config = {
|
||||||
plugins: {
|
plugins: {
|
||||||
"@tailwindcss/postcss": {},
|
"@tailwindcss/postcss": {},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
|||||||
|
After Width: | Height: | Size: 137 KiB |
|
After Width: | Height: | Size: 174 KiB |
|
After Width: | Height: | Size: 180 KiB |
|
After Width: | Height: | Size: 152 KiB |
|
After Width: | Height: | Size: 155 KiB |
|
After Width: | Height: | Size: 138 KiB |
|
After Width: | Height: | Size: 89 KiB |
|
After Width: | Height: | Size: 147 KiB |
|
After Width: | Height: | Size: 136 KiB |
|
After Width: | Height: | Size: 138 KiB |
|
After Width: | Height: | Size: 130 KiB |
|
After Width: | Height: | Size: 124 KiB |
|
After Width: | Height: | Size: 138 KiB |
|
After Width: | Height: | Size: 133 KiB |
|
After Width: | Height: | Size: 141 KiB |
|
Before Width: | Height: | Size: 218 KiB After Width: | Height: | Size: 222 KiB |
@@ -1,44 +1,44 @@
|
|||||||
{
|
{
|
||||||
"name": "OpenCut",
|
"name": "OpenCut",
|
||||||
"description": "A simple but powerful video editor that gets the job done. In your browser.",
|
"description": "A simple but powerful video editor that gets the job done. In your browser.",
|
||||||
"display": "standalone",
|
"display": "standalone",
|
||||||
"start_url": "/",
|
"start_url": "/",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "/icons/android-icon-36x36.png",
|
"src": "/icons/android-icon-36x36.png",
|
||||||
"sizes": "36x36",
|
"sizes": "36x36",
|
||||||
"type": "image\/png",
|
"type": "image\/png",
|
||||||
"density": "0.75"
|
"density": "0.75"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "/icons/android-icon-48x48.png",
|
"src": "/icons/android-icon-48x48.png",
|
||||||
"sizes": "48x48",
|
"sizes": "48x48",
|
||||||
"type": "image\/png",
|
"type": "image\/png",
|
||||||
"density": "1.0"
|
"density": "1.0"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "/icons/android-icon-72x72.png",
|
"src": "/icons/android-icon-72x72.png",
|
||||||
"sizes": "72x72",
|
"sizes": "72x72",
|
||||||
"type": "image\/png",
|
"type": "image\/png",
|
||||||
"density": "1.5"
|
"density": "1.5"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "/icons/android-icon-96x96.png",
|
"src": "/icons/android-icon-96x96.png",
|
||||||
"sizes": "96x96",
|
"sizes": "96x96",
|
||||||
"type": "image\/png",
|
"type": "image\/png",
|
||||||
"density": "2.0"
|
"density": "2.0"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "/icons/android-icon-144x144.png",
|
"src": "/icons/android-icon-144x144.png",
|
||||||
"sizes": "144x144",
|
"sizes": "144x144",
|
||||||
"type": "image\/png",
|
"type": "image\/png",
|
||||||
"density": "3.0"
|
"density": "3.0"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "/icons/android-icon-192x192.png",
|
"src": "/icons/android-icon-192x192.png",
|
||||||
"sizes": "192x192",
|
"sizes": "192x192",
|
||||||
"type": "image\/png",
|
"type": "image\/png",
|
||||||
"density": "4.0"
|
"density": "4.0"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
||||||
|
<circle cx="50" cy="50" r="44" fill="currentColor" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 126 B |
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
||||||
|
<polygon points="50,6 94,50 50,94 6,50" fill="currentColor" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 135 B |
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
||||||
|
<polygon points="28,8 72,8 94,50 72,92 28,92 6,50" fill="currentColor" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 146 B |
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
||||||
|
<rect x="10" y="10" width="80" height="80" fill="currentColor" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 138 B |
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
||||||
|
<polygon points="50,6 61,37 94,37 67,56 77,88 50,68 23,88 33,56 6,37 39,37" fill="currentColor" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 171 B |
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
||||||
|
<polygon points="50,8 94,92 6,92" fill="currentColor" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 129 B |
@@ -0,0 +1,281 @@
|
|||||||
|
/**
|
||||||
|
* Generates font sprite atlas for the font picker.
|
||||||
|
*
|
||||||
|
* Downloads Google Fonts from Fontsource, renders each font name as a sprite,
|
||||||
|
* packs them into chunk images, and outputs a JSON atlas + AVIF images.
|
||||||
|
*
|
||||||
|
* Run: npx tsx scripts/generate-font-sprites.ts
|
||||||
|
* Deps: @napi-rs/canvas sharp (install as devDependencies)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { createCanvas, GlobalFonts } from "@napi-rs/canvas";
|
||||||
|
import sharp from "sharp";
|
||||||
|
import { mkdir, writeFile, readFile } from "node:fs/promises";
|
||||||
|
import { existsSync } from "node:fs";
|
||||||
|
import { join, dirname } from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
|
const FONT_SIZE = 24;
|
||||||
|
const ROW_HEIGHT = 40;
|
||||||
|
const CANVAS_WIDTH = 1200;
|
||||||
|
const MAX_CHUNK_HEIGHT = 800;
|
||||||
|
const PADDING_X = 14;
|
||||||
|
const WIDTH_BUFFER = 8;
|
||||||
|
const CONCURRENT_DOWNLOADS = 30;
|
||||||
|
|
||||||
|
const OUTPUT_DIR = join(__dirname, "..", "public", "fonts");
|
||||||
|
const CACHE_DIR = join(__dirname, "..", ".font-cache");
|
||||||
|
const FONTSOURCE_API = "https://api.fontsource.org/v1/fonts";
|
||||||
|
|
||||||
|
interface FontsourceFont {
|
||||||
|
id: string;
|
||||||
|
family: string;
|
||||||
|
subsets: string[];
|
||||||
|
weights: number[];
|
||||||
|
styles: string[];
|
||||||
|
category: string;
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MeasuredFont {
|
||||||
|
family: string;
|
||||||
|
width: number;
|
||||||
|
styles: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PackedFont {
|
||||||
|
family: string;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
w: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AtlasEntry {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
w: number;
|
||||||
|
ch: number;
|
||||||
|
s: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchFontList(): Promise<FontsourceFont[]> {
|
||||||
|
console.log("Fetching font list from Fontsource...");
|
||||||
|
const response = await fetch(FONTSOURCE_API);
|
||||||
|
if (!response.ok) throw new Error(`Fontsource API error: ${response.status}`);
|
||||||
|
|
||||||
|
const data: FontsourceFont[] = await response.json();
|
||||||
|
const filtered = data.filter(
|
||||||
|
(font) =>
|
||||||
|
font.type === "google" &&
|
||||||
|
font.subsets.includes("latin") &&
|
||||||
|
font.weights.includes(400),
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
` ${filtered.length} Google fonts with Latin subset + 400 weight`,
|
||||||
|
);
|
||||||
|
return filtered;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function downloadFont({ id }: { id: string }): Promise<Buffer | null> {
|
||||||
|
const cachePath = join(CACHE_DIR, `${id}.woff2`);
|
||||||
|
|
||||||
|
if (existsSync(cachePath)) {
|
||||||
|
return readFile(cachePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fontsource CDN serves woff2 for all Google fonts
|
||||||
|
const url = `https://cdn.jsdelivr.net/fontsource/fonts/${id}@latest/latin-400-normal.woff2`;
|
||||||
|
try {
|
||||||
|
const response = await fetch(url);
|
||||||
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||||
|
const buffer = Buffer.from(await response.arrayBuffer());
|
||||||
|
await writeFile(cachePath, buffer);
|
||||||
|
return buffer;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function downloadAllFonts({
|
||||||
|
fonts,
|
||||||
|
concurrency,
|
||||||
|
}: {
|
||||||
|
fonts: FontsourceFont[];
|
||||||
|
concurrency: number;
|
||||||
|
}): Promise<Map<string, Buffer>> {
|
||||||
|
console.log(
|
||||||
|
`Downloading ${fonts.length} font files (${concurrency} concurrent)...`,
|
||||||
|
);
|
||||||
|
const results = new Map<string, Buffer>();
|
||||||
|
let nextIndex = 0;
|
||||||
|
let completed = 0;
|
||||||
|
|
||||||
|
async function worker() {
|
||||||
|
while (nextIndex < fonts.length) {
|
||||||
|
const index = nextIndex++;
|
||||||
|
const font = fonts[index];
|
||||||
|
const buffer = await downloadFont({ id: font.id });
|
||||||
|
if (buffer) results.set(font.id, buffer);
|
||||||
|
completed++;
|
||||||
|
if (completed % 100 === 0 || completed === fonts.length) {
|
||||||
|
process.stdout.write(`\r ${completed}/${fonts.length}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(Array.from({ length: concurrency }, () => worker()));
|
||||||
|
console.log(`\n Downloaded ${results.size}/${fonts.length} fonts`);
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
function measureFonts({
|
||||||
|
fonts,
|
||||||
|
fontBuffers,
|
||||||
|
}: {
|
||||||
|
fonts: FontsourceFont[];
|
||||||
|
fontBuffers: Map<string, Buffer>;
|
||||||
|
}): MeasuredFont[] {
|
||||||
|
console.log("Registering fonts and measuring text...");
|
||||||
|
const measured: MeasuredFont[] = [];
|
||||||
|
const canvas = createCanvas(CANVAS_WIDTH, ROW_HEIGHT);
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
|
|
||||||
|
for (const font of fonts) {
|
||||||
|
const buffer = fontBuffers.get(font.id);
|
||||||
|
if (!buffer) continue;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const ok = GlobalFonts.register(buffer, font.family);
|
||||||
|
if (!ok) continue;
|
||||||
|
|
||||||
|
ctx.font = `${FONT_SIZE}px "${font.family}"`;
|
||||||
|
const metrics = ctx.measureText(font.family);
|
||||||
|
const width = Math.ceil(metrics.width) + WIDTH_BUFFER;
|
||||||
|
|
||||||
|
const styles: string[] = [];
|
||||||
|
for (const weight of font.weights) {
|
||||||
|
if (font.styles.includes("normal")) styles.push(String(weight));
|
||||||
|
if (font.styles.includes("italic")) styles.push(`${weight}i`);
|
||||||
|
}
|
||||||
|
|
||||||
|
measured.push({ family: font.family, width, styles });
|
||||||
|
} catch {
|
||||||
|
// skip fonts that fail to register
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
measured.sort((a, b) => a.family.localeCompare(b.family));
|
||||||
|
console.log(` ${measured.length} fonts measured`);
|
||||||
|
return measured;
|
||||||
|
}
|
||||||
|
|
||||||
|
function packIntoChunks({ measured }: { measured: MeasuredFont[] }): {
|
||||||
|
atlas: Record<string, AtlasEntry>;
|
||||||
|
chunks: PackedFont[][];
|
||||||
|
} {
|
||||||
|
console.log("Packing into sprite chunks...");
|
||||||
|
const atlas: Record<string, AtlasEntry> = {};
|
||||||
|
const chunks: PackedFont[][] = [[]];
|
||||||
|
let chunkIndex = 0;
|
||||||
|
let cursorX = 0;
|
||||||
|
let cursorY = 0;
|
||||||
|
|
||||||
|
for (const font of measured) {
|
||||||
|
// New row if doesn't fit horizontally
|
||||||
|
if (cursorX + font.width > CANVAS_WIDTH) {
|
||||||
|
cursorX = 0;
|
||||||
|
cursorY += ROW_HEIGHT;
|
||||||
|
}
|
||||||
|
|
||||||
|
// New chunk if doesn't fit vertically
|
||||||
|
if (cursorY + ROW_HEIGHT > MAX_CHUNK_HEIGHT) {
|
||||||
|
chunkIndex++;
|
||||||
|
chunks.push([]);
|
||||||
|
cursorX = 0;
|
||||||
|
cursorY = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same data goes to BOTH the atlas and the chunk render list
|
||||||
|
const x = cursorX;
|
||||||
|
const y = cursorY;
|
||||||
|
|
||||||
|
atlas[font.family] = {
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
w: font.width,
|
||||||
|
ch: chunkIndex,
|
||||||
|
s: font.styles,
|
||||||
|
};
|
||||||
|
chunks[chunkIndex].push({ family: font.family, x, y, w: font.width });
|
||||||
|
|
||||||
|
cursorX += font.width + PADDING_X;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(` ${chunks.length} chunks`);
|
||||||
|
return { atlas, chunks };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function renderChunks({
|
||||||
|
chunks,
|
||||||
|
}: {
|
||||||
|
chunks: PackedFont[][];
|
||||||
|
}): Promise<void> {
|
||||||
|
console.log("Rendering sprite chunks...");
|
||||||
|
|
||||||
|
for (let i = 0; i < chunks.length; i++) {
|
||||||
|
const chunk = chunks[i];
|
||||||
|
const chunkHeight = Math.max(...chunk.map((f) => f.y)) + ROW_HEIGHT;
|
||||||
|
|
||||||
|
const canvas = createCanvas(CANVAS_WIDTH, chunkHeight);
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
|
|
||||||
|
for (const font of chunk) {
|
||||||
|
ctx.font = `${FONT_SIZE}px "${font.family}"`;
|
||||||
|
ctx.fillStyle = "#000000";
|
||||||
|
ctx.textBaseline = "middle";
|
||||||
|
ctx.fillText(font.family, font.x, font.y + ROW_HEIGHT / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
const pngBuffer = canvas.toBuffer("image/png");
|
||||||
|
await sharp(pngBuffer)
|
||||||
|
.avif({ quality: 80 })
|
||||||
|
.toFile(join(OUTPUT_DIR, `font-chunk-${i}.avif`));
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
` Chunk ${i}: ${chunk.length} fonts, ${CANVAS_WIDTH}×${chunkHeight}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
await mkdir(OUTPUT_DIR, { recursive: true });
|
||||||
|
await mkdir(CACHE_DIR, { recursive: true });
|
||||||
|
|
||||||
|
const fonts = await fetchFontList();
|
||||||
|
const fontBuffers = await downloadAllFonts({
|
||||||
|
fonts,
|
||||||
|
concurrency: CONCURRENT_DOWNLOADS,
|
||||||
|
});
|
||||||
|
const measured = measureFonts({ fonts, fontBuffers });
|
||||||
|
const { atlas, chunks } = packIntoChunks({ measured });
|
||||||
|
await renderChunks({ chunks });
|
||||||
|
|
||||||
|
// Write atlas JSON (compact for smaller file size)
|
||||||
|
await writeFile(
|
||||||
|
join(OUTPUT_DIR, "font-atlas.json"),
|
||||||
|
JSON.stringify({ fonts: atlas }),
|
||||||
|
);
|
||||||
|
|
||||||
|
const totalFonts = Object.keys(atlas).length;
|
||||||
|
console.log(
|
||||||
|
`\nDone! ${totalFonts} fonts in ${chunks.length} chunks → ${OUTPUT_DIR}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((error) => {
|
||||||
|
console.error("Failed:", error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -88,18 +88,17 @@ function PostHeader({ post }: { post: Post }) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="flex flex-col items-center justify-center gap-8">
|
||||||
{post.coverImage && <PostCoverImage post={post} />}
|
|
||||||
<PostMeta date={formattedDate} publishedAt={post.publishedAt} />
|
<PostMeta date={formattedDate} publishedAt={post.publishedAt} />
|
||||||
<PostTitle title={post.title} />
|
<PostTitle title={post.title} />
|
||||||
<PostAuthor author={post.authors[0]} />
|
{post.coverImage && <PostCoverImage post={post} />}
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function PostCoverImage({ post }: { post: Post }) {
|
function PostCoverImage({ post }: { post: Post }) {
|
||||||
return (
|
return (
|
||||||
<div className="relative aspect-video overflow-hidden rounded-lg">
|
<div className="relative aspect-video overflow-hidden rounded-lg w-full mt-4">
|
||||||
<Image
|
<Image
|
||||||
src={post.coverImage}
|
src={post.coverImage}
|
||||||
alt={post.title}
|
alt={post.title}
|
||||||
@@ -121,31 +120,15 @@ function PostMeta({ date, publishedAt }: { date: string; publishedAt: Date }) {
|
|||||||
|
|
||||||
function PostTitle({ title }: { title: string }) {
|
function PostTitle({ title }: { title: string }) {
|
||||||
return (
|
return (
|
||||||
<h1 className="text-5xl font-bold tracking-tight md:text-4xl">{title}</h1>
|
<h1 className="text-5xl font-bold tracking-tight md:text-4xl text-center">
|
||||||
);
|
{title}
|
||||||
}
|
</h1>
|
||||||
|
|
||||||
function PostAuthor({ author }: { author?: Author }) {
|
|
||||||
if (!author) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-center gap-2">
|
|
||||||
<Image
|
|
||||||
src={author.image}
|
|
||||||
alt={author.name}
|
|
||||||
width={36}
|
|
||||||
height={36}
|
|
||||||
loading="eager"
|
|
||||||
className="aspect-square size-8 shrink-0 rounded-full"
|
|
||||||
/>
|
|
||||||
<p className="text-muted-foreground">{author.name}</p>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function PostContent({ html }: { html: string }) {
|
function PostContent({ html }: { html: string }) {
|
||||||
return (
|
return (
|
||||||
<section className="pt-8">
|
<section className="">
|
||||||
<Prose html={html} />
|
<Prose html={html} />
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { BasePage } from "@/app/base-page";
|
import { BasePage } from "@/app/base-page";
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { getPosts } from "@/lib/blog/query";
|
import { getPosts } from "@/lib/blog/query";
|
||||||
import type { Author, Post } from "@/types/blog";
|
import type { Post } from "@/types/blog";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Blog - OpenCut",
|
title: "Blog - OpenCut",
|
||||||
@@ -47,27 +46,7 @@ function BlogPostItem({ post }: { post: Post }) {
|
|||||||
<h2 className="text-xl font-semibold">{post.title}</h2>
|
<h2 className="text-xl font-semibold">{post.title}</h2>
|
||||||
<p className="text-muted-foreground">{post.description}</p>
|
<p className="text-muted-foreground">{post.description}</p>
|
||||||
</div>
|
</div>
|
||||||
{post.authors && post.authors.length > 0 && (
|
|
||||||
<AuthorList authors={post.authors} />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function AuthorList({ authors }: { authors: Author[] }) {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{authors.map((author) => (
|
|
||||||
<div key={author.id} className="flex items-center gap-2">
|
|
||||||
<Avatar className="size-6 shadow-sm">
|
|
||||||
<AvatarImage src={author.image} alt={author.name} />
|
|
||||||
<AvatarFallback className="text-xs">
|
|
||||||
{author.name.charAt(0).toUpperCase()}
|
|
||||||
</AvatarFallback>
|
|
||||||
</Avatar>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -8,13 +8,14 @@ import {
|
|||||||
} from "@/components/ui/resizable";
|
} from "@/components/ui/resizable";
|
||||||
import { AssetsPanel } from "@/components/editor/panels/assets";
|
import { AssetsPanel } from "@/components/editor/panels/assets";
|
||||||
import { PropertiesPanel } from "@/components/editor/panels/properties";
|
import { PropertiesPanel } from "@/components/editor/panels/properties";
|
||||||
import { Timeline } from "@/components/editor/timeline";
|
import { Timeline } from "@/components/editor/panels/timeline";
|
||||||
import { PreviewPanel } from "@/components/editor/panels/preview";
|
import { PreviewPanel } from "@/components/editor/panels/preview";
|
||||||
import { EditorHeader } from "@/components/editor/editor-header";
|
import { EditorHeader } from "@/components/editor/editor-header";
|
||||||
import { EditorProvider } from "@/components/providers/editor-provider";
|
import { EditorProvider } from "@/components/providers/editor-provider";
|
||||||
import { Onboarding } from "@/components/editor/onboarding";
|
import { Onboarding } from "@/components/editor/onboarding";
|
||||||
import { MigrationDialog } from "@/components/editor/dialogs/migration-dialog";
|
import { MigrationDialog } from "@/components/editor/dialogs/migration-dialog";
|
||||||
import { usePanelStore } from "@/stores/panel-store";
|
import { usePanelStore } from "@/stores/panel-store";
|
||||||
|
import { usePasteMedia } from "@/hooks/use-paste-media";
|
||||||
|
|
||||||
export default function Editor() {
|
export default function Editor() {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
@@ -35,6 +36,7 @@ export default function Editor() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function EditorLayout() {
|
function EditorLayout() {
|
||||||
|
usePasteMedia();
|
||||||
const { panels, setPanel } = usePanelStore();
|
const { panels, setPanel } = usePanelStore();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -65,7 +67,7 @@ function EditorLayout() {
|
|||||||
defaultSize={panels.tools}
|
defaultSize={panels.tools}
|
||||||
minSize={15}
|
minSize={15}
|
||||||
maxSize={40}
|
maxSize={40}
|
||||||
className="min-w-0 rounded-sm"
|
className="min-w-0"
|
||||||
>
|
>
|
||||||
<AssetsPanel />
|
<AssetsPanel />
|
||||||
</ResizablePanel>
|
</ResizablePanel>
|
||||||
@@ -86,7 +88,7 @@ function EditorLayout() {
|
|||||||
defaultSize={panels.properties}
|
defaultSize={panels.properties}
|
||||||
minSize={15}
|
minSize={15}
|
||||||
maxSize={40}
|
maxSize={40}
|
||||||
className="min-w-0 rounded-sm"
|
className="min-w-0"
|
||||||
>
|
>
|
||||||
<PropertiesPanel />
|
<PropertiesPanel />
|
||||||
</ResizablePanel>
|
</ResizablePanel>
|
||||||
|
|||||||
@@ -13,22 +13,23 @@
|
|||||||
--card: hsl(0, 0%, 100%);
|
--card: hsl(0, 0%, 100%);
|
||||||
--card-foreground: hsl(0 0% 11%);
|
--card-foreground: hsl(0 0% 11%);
|
||||||
--popover: hsl(0, 0%, 100%);
|
--popover: hsl(0, 0%, 100%);
|
||||||
|
--popover-hover: hsl(0, 0%, 96%);
|
||||||
--popover-foreground: hsl(0 0% 2%);
|
--popover-foreground: hsl(0 0% 2%);
|
||||||
--primary: hsl(203, 100%, 50%);
|
--primary: #009dff;
|
||||||
--primary-hover: hsl(203, 100%, 45%);
|
|
||||||
--primary-foreground: hsl(0, 0%, 100%);
|
--primary-foreground: hsl(0, 0%, 100%);
|
||||||
--secondary: hsl(216 13% 94%);
|
--secondary: hsl(204, 100%, 97%);
|
||||||
--secondary-foreground: hsl(0 0% 2%);
|
--secondary-border: hsl(204, 100%, 94%);
|
||||||
|
--secondary-foreground: hsl(200, 98%, 39%);
|
||||||
--muted: hsl(0 0% 85.1%);
|
--muted: hsl(0 0% 85.1%);
|
||||||
--muted-foreground: hsl(0 0% 50%);
|
--muted-foreground: hsl(0 0% 50%);
|
||||||
--accent: hsl(216, 13%, 88%);
|
--accent: hsl(0, 0%, 96%);
|
||||||
--accent-foreground: hsl(0 0% 2%);
|
--accent-foreground: hsl(0 0% 2%);
|
||||||
--destructive: hsl(0, 83%, 50%);
|
--destructive: hsl(0, 83%, 50%);
|
||||||
--destructive-foreground: hsl(0, 0%, 100%);
|
--destructive-foreground: hsl(0, 0%, 100%);
|
||||||
--constructive: hsl(141, 71%, 48%);
|
--constructive: hsl(141, 71%, 48%);
|
||||||
--constructive-foreground: hsl(0, 0%, 100%);
|
--constructive-foreground: hsl(0, 0%, 100%);
|
||||||
--border: hsl(0 0% 88%);
|
--border: hsl(0 0% 91%);
|
||||||
--input: hsl(0 0% 85.1%);
|
--input: hsl(0, 0%, 100%);
|
||||||
--ring: hsl(0, 0%, 55%);
|
--ring: hsl(0, 0%, 55%);
|
||||||
--chart-1: hsl(220 70% 50%);
|
--chart-1: hsl(220 70% 50%);
|
||||||
--chart-2: hsl(160 60% 45%);
|
--chart-2: hsl(160 60% 45%);
|
||||||
@@ -43,49 +44,75 @@
|
|||||||
--sidebar-accent-foreground: hsl(0 0% 2%);
|
--sidebar-accent-foreground: hsl(0 0% 2%);
|
||||||
--sidebar-border: hsl(0 0% 85.1%);
|
--sidebar-border: hsl(0 0% 85.1%);
|
||||||
--sidebar-ring: hsl(0 0% 16.9%);
|
--sidebar-ring: hsl(0 0% 16.9%);
|
||||||
--panel-background: hsl(216 13% 94%);
|
|
||||||
--panel-accent: hsl(216, 8%, 88%);
|
|
||||||
--sidebar: hsl(0 0% 98%);
|
--sidebar: hsl(0 0% 98%);
|
||||||
}
|
}
|
||||||
.dark {
|
|
||||||
--background: hsl(0 0% 4%);
|
.panel {
|
||||||
--foreground: hsl(0 0% 89%);
|
--background: hsl(216 13% 98%);
|
||||||
--card: hsl(0 0% 4%);
|
--foreground: hsl(0 0% 13%);
|
||||||
--card-foreground: hsl(0 0% 89%);
|
--card: hsl(0, 0%, 98%);
|
||||||
--popover: hsl(0 0% 14.9%);
|
--card-foreground: hsl(0 0% 13%);
|
||||||
--popover-foreground: hsl(0 0% 98%);
|
--primary-foreground: hsl(0, 0%, 98%);
|
||||||
--primary: hsl(203, 100%, 50%);
|
--secondary: hsl(204, 100%, 95%);
|
||||||
--primary-hover: hsl(203, 100%, 45%);
|
--secondary-border: hsl(204, 100%, 92%);
|
||||||
--primary-foreground: hsl(0 0% 9%);
|
--secondary-foreground: hsl(200, 98%, 37%);
|
||||||
--secondary: hsl(0 0% 14.9%);
|
--muted: hsl(0 0% 83.1%);
|
||||||
--secondary-foreground: hsl(0 0% 98%);
|
--muted-foreground: hsl(0 0% 48%);
|
||||||
--muted: hsl(0 0% 14.9%);
|
--accent: hsl(0, 0%, 93%);
|
||||||
--muted-foreground: hsl(0 0% 63.9%);
|
--accent-foreground: hsl(0 0% 5%);
|
||||||
--accent: hsl(0, 0%, 28%);
|
--destructive: hsl(0, 83%, 50%);
|
||||||
--accent-foreground: hsl(0 0% 98%);
|
--destructive-foreground: hsl(0, 0%, 98%);
|
||||||
--destructive: hsl(0 83%, 55%);
|
|
||||||
--destructive-foreground: hsl(0 0% 98%);
|
|
||||||
--constructive: hsl(141, 71%, 48%);
|
--constructive: hsl(141, 71%, 48%);
|
||||||
--constructive-foreground: hsl(0 0% 100%);
|
--constructive-foreground: hsl(0, 0%, 98%);
|
||||||
--border: hsl(0 0% 17%);
|
--border: hsl(0 0% 89%);
|
||||||
--input: hsl(0 0% 14.9%);
|
--input: hsl(0 0% 93%);
|
||||||
--ring: hsl(0 0% 83.1%);
|
--ring: hsl(0, 0%, 53%);
|
||||||
--chart-1: hsl(220 70% 50%);
|
}
|
||||||
--chart-2: hsl(160 60% 45%);
|
|
||||||
--chart-3: hsl(30 80% 55%);
|
.dark {
|
||||||
--chart-4: hsl(280 65% 60%);
|
--background: hsl(0, 0%, 5%);
|
||||||
--chart-5: hsl(340 75% 55%);
|
--foreground: hsl(0 0% 87%);
|
||||||
--sidebar-background: hsl(0 0% 3.9%);
|
--card: hsl(0, 0%, 5%);
|
||||||
--sidebar-foreground: hsl(0 0% 98%);
|
--card-foreground: hsl(0 0% 87%);
|
||||||
--sidebar-primary: hsl(0 0% 98%);
|
--popover: hsl(0, 0%, 5%);
|
||||||
--sidebar-primary-foreground: hsl(0 0% 9%);
|
--popover-hover: hsl(0, 0%, 22%);
|
||||||
--sidebar-accent: hsl(0 0% 14.9%);
|
--popover-foreground: hsl(0 0% 95%);
|
||||||
--sidebar-accent-foreground: hsl(0 0% 98%);
|
--secondary: hsl(204, 100%, 12%);
|
||||||
--sidebar-border: hsl(0 0% 14.9%);
|
--secondary-border: hsl(204, 100%, 15%);
|
||||||
|
--secondary-foreground: hsl(200, 98%, 61%);
|
||||||
|
--muted: hsl(0 0% 20%);
|
||||||
|
--accent: hsl(0, 0%, 14%);
|
||||||
|
--accent-foreground: hsl(0 0% 95%);
|
||||||
|
--constructive: hsl(141, 71%, 52%);
|
||||||
|
--border: hsl(0 0% 16%);
|
||||||
|
--input: hsl(0 0% 5%);
|
||||||
|
--ring: hsl(0, 0%, 50%);
|
||||||
|
--sidebar-background: hsl(0 0% 8%);
|
||||||
|
--sidebar-foreground: hsl(0 0% 95%);
|
||||||
|
--sidebar-primary: hsl(0 0% 95%);
|
||||||
|
--sidebar-primary-foreground: hsl(0 0% 15%);
|
||||||
|
--sidebar-accent: hsl(0 0% 20%);
|
||||||
|
--sidebar-accent-foreground: hsl(0 0% 95%);
|
||||||
|
--sidebar-border: hsl(0 0% 20%);
|
||||||
--sidebar-ring: hsl(0 0% 83.1%);
|
--sidebar-ring: hsl(0 0% 83.1%);
|
||||||
--panel-background: hsl(0 0% 11%);
|
--sidebar: hsl(0 0% 6%);
|
||||||
--panel-accent: hsl(0 0% 15%);
|
}
|
||||||
--sidebar: hsl(240 5.9% 10%);
|
|
||||||
|
.dark .panel {
|
||||||
|
--background: hsl(0 0% 10%);
|
||||||
|
--foreground: hsl(0 0% 85%);
|
||||||
|
--card: hsl(0, 0%, 10%);
|
||||||
|
--card-foreground: hsl(0 0% 85%);
|
||||||
|
--secondary: hsl(204, 100%, 12%);
|
||||||
|
--secondary-border: hsl(204, 100%, 17%);
|
||||||
|
--secondary-foreground: hsl(200, 98%, 63%);
|
||||||
|
--muted: hsl(0 0% 22%);
|
||||||
|
--accent: hsl(0, 0%, 15%);
|
||||||
|
--accent-foreground: hsl(0 0% 93%);
|
||||||
|
--constructive: hsl(141, 71%, 52%);
|
||||||
|
--border: hsl(0 0% 18%);
|
||||||
|
--input: hsl(0 0% 22%);
|
||||||
|
--ring: hsl(0, 0%, 52%);
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
@@ -113,6 +140,9 @@
|
|||||||
/* Prevent back/forward swipe */
|
/* Prevent back/forward swipe */
|
||||||
overscroll-behavior-x: contain;
|
overscroll-behavior-x: contain;
|
||||||
}
|
}
|
||||||
|
::selection {
|
||||||
|
@apply bg-primary/35 selection:text-primary-foreground;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@theme inline {
|
@theme inline {
|
||||||
@@ -123,10 +153,10 @@
|
|||||||
--font-sans: var(--font-inter), sans-serif;
|
--font-sans: var(--font-inter), sans-serif;
|
||||||
|
|
||||||
/* Font sizes */
|
/* Font sizes */
|
||||||
--text-xl: 1.20rem;
|
--text-xs: 0.72rem;
|
||||||
--text-base: 0.92rem;
|
--text-sm: 0.79rem;
|
||||||
|
--text-base: 0.92rem;
|
||||||
--text-base--line-height: calc(1.5 / 0.95);
|
--text-base--line-height: calc(1.5 / 0.95);
|
||||||
--text-xs: 0.75rem;
|
|
||||||
--text-xs--line-height: calc(1 / 0.8);
|
--text-xs--line-height: calc(1 / 0.8);
|
||||||
|
|
||||||
/* Border radius */
|
/* Border radius */
|
||||||
@@ -142,12 +172,13 @@
|
|||||||
--color-card-foreground: var(--card-foreground);
|
--color-card-foreground: var(--card-foreground);
|
||||||
|
|
||||||
--color-popover: var(--popover);
|
--color-popover: var(--popover);
|
||||||
|
--color-popover-hover: var(--popover-hover);
|
||||||
--color-popover-foreground: var(--popover-foreground);
|
--color-popover-foreground: var(--popover-foreground);
|
||||||
|
|
||||||
--color-primary: var(--primary);
|
--color-primary: var(--primary);
|
||||||
--color-primary-foreground: var(--primary-foreground);
|
--color-primary-foreground: var(--primary-foreground);
|
||||||
--color-primary-hover: var(--primary-hover);
|
|
||||||
--color-secondary: var(--secondary);
|
--color-secondary: var(--secondary);
|
||||||
|
--color-secondary-border: var(--secondary-border);
|
||||||
--color-secondary-foreground: var(--secondary-foreground);
|
--color-secondary-foreground: var(--secondary-foreground);
|
||||||
|
|
||||||
--color-muted: var(--muted);
|
--color-muted: var(--muted);
|
||||||
@@ -183,10 +214,6 @@
|
|||||||
--color-sidebar-border: var(--sidebar-border);
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
--color-sidebar-ring: var(--sidebar-ring);
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
|
|
||||||
/* Panel */
|
|
||||||
--color-panel: var(--panel-background);
|
|
||||||
--color-panel-accent: var(--panel-accent);
|
|
||||||
|
|
||||||
/* Animations */
|
/* Animations */
|
||||||
--animate-accordion-down: accordion-down 0.2s ease-out;
|
--animate-accordion-down: accordion-down 0.2s ease-out;
|
||||||
--animate-accordion-up: accordion-up 0.2s ease-out;
|
--animate-accordion-up: accordion-up 0.2s ease-out;
|
||||||
|
|||||||
@@ -71,6 +71,11 @@ const formatProjectDuration = ({
|
|||||||
return formatTimeCode({ timeInSeconds: duration, format });
|
return formatTimeCode({ timeInSeconds: duration, format });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const VIEW_MODE_OPTIONS = [
|
||||||
|
{ mode: "grid" as const, icon: GridViewIcon, label: "Grid view" },
|
||||||
|
{ mode: "list" as const, icon: LeftToRightListDashIcon, label: "List view" },
|
||||||
|
];
|
||||||
|
|
||||||
export default function ProjectsPage() {
|
export default function ProjectsPage() {
|
||||||
const { searchQuery, sortKey, sortOrder, viewMode } = useProjectsStore();
|
const { searchQuery, sortKey, sortOrder, viewMode } = useProjectsStore();
|
||||||
const editor = useEditor();
|
const editor = useEditor();
|
||||||
@@ -147,34 +152,23 @@ function ProjectsHeader() {
|
|||||||
</BreadcrumbList>
|
</BreadcrumbList>
|
||||||
</Breadcrumb>
|
</Breadcrumb>
|
||||||
|
|
||||||
<div className="hidden md:flex rounded-md border p-1 h-10">
|
<div className="hidden md:flex items-center rounded-md border p-1 px-1.5 h-10">
|
||||||
<button
|
{VIEW_MODE_OPTIONS.map(({ mode, icon, label }) => (
|
||||||
type="button"
|
<Button
|
||||||
className={`p-2 rounded-sm cursor-pointer ${isHydrated && viewMode === "grid" ? "bg-accent/75" : ""}`}
|
key={mode}
|
||||||
onClick={() => setViewMode({ viewMode: "grid" })}
|
variant="ghost"
|
||||||
onKeyDown={(event) =>
|
size="icon"
|
||||||
event.key === "Enter" && setViewMode({ viewMode: "grid" })
|
className={cn(
|
||||||
}
|
"rounded-sm hover:bg-background",
|
||||||
aria-label="Grid view"
|
isHydrated && viewMode === mode && "!bg-accent",
|
||||||
aria-pressed={isHydrated && viewMode === "grid"}
|
)}
|
||||||
>
|
onClick={() => setViewMode({ viewMode: mode })}
|
||||||
<HugeiconsIcon icon={GridViewIcon} className="size-4" />
|
aria-label={label}
|
||||||
</button>
|
aria-pressed={isHydrated && viewMode === mode}
|
||||||
<button
|
>
|
||||||
type="button"
|
<HugeiconsIcon icon={icon} className="size-4" />
|
||||||
className={`p-2 rounded-sm cursor-pointer ${isHydrated && viewMode === "list" ? "bg-accent/75" : ""}`}
|
</Button>
|
||||||
onClick={() => setViewMode({ viewMode: "list" })}
|
))}
|
||||||
onKeyDown={(event) =>
|
|
||||||
event.key === "Enter" && setViewMode({ viewMode: "list" })
|
|
||||||
}
|
|
||||||
aria-label="List view"
|
|
||||||
aria-pressed={isHydrated && viewMode === "list"}
|
|
||||||
>
|
|
||||||
<HugeiconsIcon
|
|
||||||
icon={LeftToRightListDashIcon}
|
|
||||||
className="size-4"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -251,7 +245,6 @@ function ProjectsToolbar({ projectIds }: { projectIds: string[] }) {
|
|||||||
</Button>
|
</Button>
|
||||||
</SortDropdown>
|
</SortDropdown>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
|
||||||
variant="text"
|
variant="text"
|
||||||
className="text-muted-foreground"
|
className="text-muted-foreground"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
@@ -277,28 +270,21 @@ function ProjectsToolbar({ projectIds }: { projectIds: string[] }) {
|
|||||||
<div className="h-4 w-px bg-border/50 block md:hidden" />
|
<div className="h-4 w-px bg-border/50 block md:hidden" />
|
||||||
|
|
||||||
<div className="flex md:hidden items-center gap-4">
|
<div className="flex md:hidden items-center gap-4">
|
||||||
<Button
|
{VIEW_MODE_OPTIONS.map(({ mode, icon, label }) => (
|
||||||
variant="text"
|
<Button
|
||||||
onClick={() => setViewMode({ viewMode: "grid" })}
|
key={mode}
|
||||||
>
|
variant="text"
|
||||||
<HugeiconsIcon
|
onClick={() => setViewMode({ viewMode: mode })}
|
||||||
icon={GridViewIcon}
|
aria-label={label}
|
||||||
className={cn(
|
>
|
||||||
viewMode === "grid" ? "text-primary" : "text-muted-foreground",
|
<HugeiconsIcon
|
||||||
)}
|
icon={icon}
|
||||||
/>
|
className={cn(
|
||||||
</Button>
|
viewMode === mode ? "text-primary" : "text-muted-foreground",
|
||||||
<Button
|
)}
|
||||||
variant="text"
|
/>
|
||||||
onClick={() => setViewMode({ viewMode: "list" })}
|
</Button>
|
||||||
>
|
))}
|
||||||
<HugeiconsIcon
|
|
||||||
icon={LeftToRightListDashIcon}
|
|
||||||
className={cn(
|
|
||||||
viewMode === "list" ? "text-primary" : "text-muted-foreground",
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{selectedProjectCount > 0 ? <ProjectActions /> : null}
|
{selectedProjectCount > 0 ? <ProjectActions /> : null}
|
||||||
@@ -781,7 +767,6 @@ function ProjectMenu({
|
|||||||
<DropdownMenu open={isOpen} onOpenChange={onOpenChange}>
|
<DropdownMenu open={isOpen} onOpenChange={onOpenChange}>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
|
||||||
variant="background"
|
variant="background"
|
||||||
className={
|
className={
|
||||||
isGrid
|
isGrid
|
||||||
@@ -862,7 +847,7 @@ function ProjectsSkeleton() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="xs:grid-cols-2 grid grid-cols-1 gap-6 sm:grid-cols-3 lg:grid-cols-4">
|
<div className="px-4 xs:grid-cols-2 grid grid-cols-1 gap-6 sm:grid-cols-3 lg:grid-cols-4">
|
||||||
{skeletonIds.map((skeletonId) => (
|
{skeletonIds.map((skeletonId) => (
|
||||||
<Card
|
<Card
|
||||||
key={skeletonId}
|
key={skeletonId}
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ export const useProjectsStore = create<ProjectsState>()(
|
|||||||
persist(
|
persist(
|
||||||
(set) => ({
|
(set) => ({
|
||||||
searchQuery: "",
|
searchQuery: "",
|
||||||
sortKey: "createdAt",
|
sortKey: "updatedAt",
|
||||||
sortOrder: "desc",
|
sortOrder: "desc",
|
||||||
viewMode: "grid",
|
viewMode: "grid",
|
||||||
selectedProjectIds: [],
|
selectedProjectIds: [],
|
||||||
@@ -86,7 +86,9 @@ export const useProjectsStore = create<ProjectsState>()(
|
|||||||
projectId,
|
projectId,
|
||||||
isSelected,
|
isSelected,
|
||||||
}),
|
}),
|
||||||
lastSelectedProjectId: isSelected ? projectId : state.lastSelectedProjectId,
|
lastSelectedProjectId: isSelected
|
||||||
|
? projectId
|
||||||
|
: state.lastSelectedProjectId,
|
||||||
})),
|
})),
|
||||||
selectProjectRange: ({ projectId, allProjectIds }) =>
|
selectProjectRange: ({ projectId, allProjectIds }) =>
|
||||||
set((state) => {
|
set((state) => {
|
||||||
@@ -120,7 +122,11 @@ export const useProjectsStore = create<ProjectsState>()(
|
|||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: "projects-view-mode",
|
name: "projects-view-mode",
|
||||||
partialize: (state) => ({ viewMode: state.viewMode }),
|
partialize: (state) => ({
|
||||||
|
viewMode: state.viewMode,
|
||||||
|
sortKey: state.sortKey,
|
||||||
|
sortOrder: state.sortOrder,
|
||||||
|
}),
|
||||||
onRehydrateStorage: () => (state) => {
|
onRehydrateStorage: () => (state) => {
|
||||||
state?.setIsHydrated({ isHydrated: true });
|
state?.setIsHydrated({ isHydrated: true });
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -122,8 +122,8 @@ export function EditableTimecode({
|
|||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
onBlur={handleBlur}
|
onBlur={handleBlur}
|
||||||
className={cn(
|
className={cn(
|
||||||
"border-none bg-transparent font-mono text-xs outline-none",
|
"-mx-1 border border-transparent bg-transparent px-1 font-mono text-xs outline-none",
|
||||||
"focus:bg-background focus:border-primary focus:rounded focus:border focus:px-1",
|
"focus:bg-background focus:border-primary focus:rounded",
|
||||||
"text-primary tabular-nums",
|
"text-primary tabular-nums",
|
||||||
hasError && "text-destructive focus:border-destructive",
|
hasError && "text-destructive focus:border-destructive",
|
||||||
className,
|
className,
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Button } from "../ui/button";
|
import { Button } from "../ui/button";
|
||||||
import { ChevronDown } from "lucide-react";
|
import { useRef, useState } from "react";
|
||||||
import { useState } from "react";
|
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
@@ -17,23 +16,21 @@ import { useRouter } from "next/navigation";
|
|||||||
import { FaDiscord } from "react-icons/fa6";
|
import { FaDiscord } from "react-icons/fa6";
|
||||||
import { ExportButton } from "./export-button";
|
import { ExportButton } from "./export-button";
|
||||||
import { ThemeToggle } from "../theme-toggle";
|
import { ThemeToggle } from "../theme-toggle";
|
||||||
import { SOCIAL_LINKS } from "@/constants/site-constants";
|
import { DEFAULT_LOGO_URL, SOCIAL_LINKS } from "@/constants/site-constants";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { useEditor } from "@/hooks/use-editor";
|
import { useEditor } from "@/hooks/use-editor";
|
||||||
import {
|
import { CommandIcon, Logout05Icon } from "@hugeicons/core-free-icons";
|
||||||
ArrowLeft02Icon,
|
|
||||||
Edit03Icon,
|
|
||||||
Delete02Icon,
|
|
||||||
CommandIcon,
|
|
||||||
} from "@hugeicons/core-free-icons";
|
|
||||||
import { HugeiconsIcon } from "@hugeicons/react";
|
import { HugeiconsIcon } from "@hugeicons/react";
|
||||||
import { ShortcutsDialog } from "./dialogs/shortcuts-dialog";
|
import { ShortcutsDialog } from "./dialogs/shortcuts-dialog";
|
||||||
|
import Image from "next/image";
|
||||||
|
import { cn } from "@/utils/ui";
|
||||||
|
|
||||||
export function EditorHeader() {
|
export function EditorHeader() {
|
||||||
return (
|
return (
|
||||||
<header className="bg-background flex h-[3.2rem] items-center justify-between px-3 pt-0.5">
|
<header className="bg-background flex h-[3.4rem] items-center justify-between px-3 pt-0.5">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-1">
|
||||||
<ProjectDropdown />
|
<ProjectDropdown />
|
||||||
|
<EditableProjectName />
|
||||||
</div>
|
</div>
|
||||||
<nav className="flex items-center gap-2">
|
<nav className="flex items-center gap-2">
|
||||||
<ExportButton />
|
<ExportButton />
|
||||||
@@ -111,56 +108,40 @@ function ProjectDropdown() {
|
|||||||
<>
|
<>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button
|
<Button variant="ghost" size="icon" className="p-1 rounded-sm size-8">
|
||||||
variant="secondary"
|
<Image
|
||||||
className="flex h-auto items-center justify-center px-2.5 py-1.5"
|
src={DEFAULT_LOGO_URL}
|
||||||
>
|
alt="Project thumbnail"
|
||||||
<ChevronDown className="text-muted-foreground" />
|
width={32}
|
||||||
<span className="mr-2 text-[0.85rem]">
|
height={32}
|
||||||
{activeProject?.metadata.name}
|
className="invert dark:invert-0 size-5"
|
||||||
</span>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="start" className="z-100 w-52">
|
<DropdownMenuContent align="start" className="z-100 w-44">
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="flex items-center gap-1.5"
|
|
||||||
onClick={handleExit}
|
onClick={handleExit}
|
||||||
disabled={isExiting}
|
disabled={isExiting}
|
||||||
|
icon={<HugeiconsIcon icon={Logout05Icon} />}
|
||||||
>
|
>
|
||||||
<HugeiconsIcon icon={ArrowLeft02Icon} className="size-4" />
|
|
||||||
Exit project
|
Exit project
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="flex items-center gap-1.5"
|
|
||||||
onClick={() => setOpenDialog("rename")}
|
|
||||||
>
|
|
||||||
<HugeiconsIcon icon={Edit03Icon} className="size-4" />
|
|
||||||
Rename project
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem
|
|
||||||
variant="destructive"
|
|
||||||
className="flex items-center gap-1.5"
|
|
||||||
onClick={() => setOpenDialog("delete")}
|
|
||||||
>
|
|
||||||
<HugeiconsIcon icon={Delete02Icon} className="size-4" />
|
|
||||||
Delete project
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuItem
|
|
||||||
className="flex items-center gap-1.5"
|
|
||||||
onClick={() => setOpenDialog("shortcuts")}
|
onClick={() => setOpenDialog("shortcuts")}
|
||||||
|
icon={<HugeiconsIcon icon={CommandIcon} />}
|
||||||
>
|
>
|
||||||
<HugeiconsIcon icon={CommandIcon} className="size-4" />
|
Shortcuts
|
||||||
Keyboard shortcuts
|
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem asChild>
|
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
|
||||||
|
<DropdownMenuItem asChild icon={<FaDiscord className="!size-4" />}>
|
||||||
<Link
|
<Link
|
||||||
href={SOCIAL_LINKS.discord}
|
href={SOCIAL_LINKS.discord}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="flex items-center gap-1.5"
|
|
||||||
>
|
>
|
||||||
<FaDiscord className="size-4" />
|
|
||||||
Discord
|
Discord
|
||||||
</Link>
|
</Link>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
@@ -185,3 +166,79 @@ function ProjectDropdown() {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function EditableProjectName() {
|
||||||
|
const editor = useEditor();
|
||||||
|
const activeProject = editor.project.getActive();
|
||||||
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const originalNameRef = useRef("");
|
||||||
|
|
||||||
|
const projectName = activeProject?.metadata.name || "";
|
||||||
|
|
||||||
|
const startEditing = () => {
|
||||||
|
if (isEditing) return;
|
||||||
|
originalNameRef.current = projectName;
|
||||||
|
setIsEditing(true);
|
||||||
|
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
inputRef.current?.select();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveEdit = async () => {
|
||||||
|
if (!inputRef.current || !activeProject) return;
|
||||||
|
const newName = inputRef.current.value.trim();
|
||||||
|
setIsEditing(false);
|
||||||
|
|
||||||
|
if (!newName) {
|
||||||
|
inputRef.current.value = originalNameRef.current;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newName !== originalNameRef.current) {
|
||||||
|
try {
|
||||||
|
await editor.project.renameProject({
|
||||||
|
id: activeProject.metadata.id,
|
||||||
|
name: newName,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
toast.error("Failed to rename project", {
|
||||||
|
description:
|
||||||
|
error instanceof Error ? error.message : "Please try again",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = (event: React.KeyboardEvent) => {
|
||||||
|
if (event.key === "Enter") {
|
||||||
|
event.preventDefault();
|
||||||
|
inputRef.current?.blur();
|
||||||
|
} else if (event.key === "Escape") {
|
||||||
|
event.preventDefault();
|
||||||
|
if (inputRef.current) {
|
||||||
|
inputRef.current.value = originalNameRef.current;
|
||||||
|
}
|
||||||
|
setIsEditing(false);
|
||||||
|
inputRef.current?.blur();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
ref={inputRef}
|
||||||
|
type="text"
|
||||||
|
defaultValue={projectName}
|
||||||
|
readOnly={!isEditing}
|
||||||
|
onClick={startEditing}
|
||||||
|
onBlur={saveEdit}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
style={{ fieldSizing: "content" }}
|
||||||
|
className={cn(
|
||||||
|
"text-[0.9rem] h-8 px-2 py-1 rounded-sm bg-transparent outline-none cursor-pointer hover:bg-accent hover:text-accent-foreground",
|
||||||
|
isEditing && "ring-1 ring-ring cursor-text hover:bg-transparent",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { Progress } from "@/components/ui/progress";
|
|||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import { cn } from "@/utils/ui";
|
import { cn } from "@/utils/ui";
|
||||||
import { getExportMimeType, getExportFileExtension } from "@/lib/export";
|
import { getExportMimeType, getExportFileExtension } from "@/lib/export";
|
||||||
import { Check, Copy, Download, RotateCcw, X } from "lucide-react";
|
import { Check, Copy, Download, RotateCcw } from "lucide-react";
|
||||||
import {
|
import {
|
||||||
EXPORT_FORMAT_VALUES,
|
EXPORT_FORMAT_VALUES,
|
||||||
EXPORT_QUALITY_VALUES,
|
EXPORT_QUALITY_VALUES,
|
||||||
@@ -23,7 +23,11 @@ import {
|
|||||||
type ExportQuality,
|
type ExportQuality,
|
||||||
type ExportResult,
|
type ExportResult,
|
||||||
} from "@/types/export";
|
} from "@/types/export";
|
||||||
import { PropertyGroup } from "@/components/editor/panels/properties/property-item";
|
import {
|
||||||
|
Section,
|
||||||
|
SectionContent,
|
||||||
|
SectionHeader,
|
||||||
|
} from "@/components/editor/panels/properties/section";
|
||||||
import { useEditor } from "@/hooks/use-editor";
|
import { useEditor } from "@/hooks/use-editor";
|
||||||
import { DEFAULT_EXPORT_OPTIONS } from "@/constants/export-constants";
|
import { DEFAULT_EXPORT_OPTIONS } from "@/constants/export-constants";
|
||||||
|
|
||||||
@@ -44,9 +48,7 @@ export function ExportButton() {
|
|||||||
type="button"
|
type="button"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center gap-1.5 rounded-md bg-[#38BDF8] px-[0.12rem] py-[0.12rem] text-white",
|
"flex items-center gap-1.5 rounded-md bg-[#38BDF8] px-[0.12rem] py-[0.12rem] text-white",
|
||||||
hasProject
|
hasProject ? "cursor-pointer" : "cursor-not-allowed opacity-50",
|
||||||
? "cursor-pointer hover:brightness-105"
|
|
||||||
: "cursor-not-allowed opacity-50",
|
|
||||||
)}
|
)}
|
||||||
onClick={hasProject ? handleExport : undefined}
|
onClick={hasProject ? handleExport : undefined}
|
||||||
disabled={!hasProject}
|
disabled={!hasProject}
|
||||||
@@ -141,20 +143,12 @@ function ExportPopover({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClose = () => {
|
|
||||||
if (!isExporting) {
|
|
||||||
onOpenChange(false);
|
|
||||||
setExportResult(null);
|
|
||||||
setProgress(0);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
cancelRequestedRef.current = true;
|
cancelRequestedRef.current = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PopoverContent className="bg-background mr-4 flex w-80 flex-col gap-3">
|
<PopoverContent className="bg-background mr-4 flex w-80 flex-col p-0">
|
||||||
{exportResult && !exportResult.success ? (
|
{exportResult && !exportResult.success ? (
|
||||||
<ExportError
|
<ExportError
|
||||||
error={exportResult.error || "Unknown error occurred"}
|
error={exportResult.error || "Unknown error occurred"}
|
||||||
@@ -162,110 +156,106 @@ function ExportPopover({
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between p-3 border-b">
|
||||||
<h3 className="font-medium">
|
<h3 className="font-medium text-sm">
|
||||||
{isExporting ? "Exporting project" : "Export project"}
|
{isExporting ? "Exporting project" : "Export project"}
|
||||||
</h3>
|
</h3>
|
||||||
<Button variant="text" size="icon" onClick={handleClose}>
|
|
||||||
<X className="text-foreground/85 !size-5" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
{!isExporting && (
|
{!isExporting && (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col">
|
||||||
<PropertyGroup
|
<Section collapsible defaultOpen={false} hasBorderTop={false}>
|
||||||
title="Format"
|
<SectionHeader title="Format" />
|
||||||
titleClassName="text-sm"
|
<SectionContent>
|
||||||
defaultExpanded={false}
|
<RadioGroup
|
||||||
>
|
value={format}
|
||||||
<RadioGroup
|
onValueChange={(value) => {
|
||||||
value={format}
|
if (isExportFormat(value)) {
|
||||||
onValueChange={(value) => {
|
setFormat(value);
|
||||||
if (isExportFormat(value)) {
|
}
|
||||||
setFormat(value);
|
}}
|
||||||
}
|
>
|
||||||
}}
|
<div className="flex items-center space-x-2">
|
||||||
>
|
<RadioGroupItem value="mp4" id="mp4" />
|
||||||
<div className="flex items-center space-x-2">
|
<Label htmlFor="mp4">
|
||||||
<RadioGroupItem value="mp4" id="mp4" />
|
MP4 (H.264) - Better compatibility
|
||||||
<Label htmlFor="mp4">
|
</Label>
|
||||||
MP4 (H.264) - Better compatibility
|
</div>
|
||||||
</Label>
|
<div className="flex items-center space-x-2">
|
||||||
</div>
|
<RadioGroupItem value="webm" id="webm" />
|
||||||
<div className="flex items-center space-x-2">
|
<Label htmlFor="webm">
|
||||||
<RadioGroupItem value="webm" id="webm" />
|
WebM (VP9) - Smaller file size
|
||||||
<Label htmlFor="webm">
|
</Label>
|
||||||
WebM (VP9) - Smaller file size
|
</div>
|
||||||
</Label>
|
</RadioGroup>
|
||||||
</div>
|
</SectionContent>
|
||||||
</RadioGroup>
|
</Section>
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<PropertyGroup
|
<Section collapsible defaultOpen={false}>
|
||||||
title="Quality"
|
<SectionHeader title="Quality" />
|
||||||
titleClassName="text-sm"
|
<SectionContent>
|
||||||
defaultExpanded={false}
|
<RadioGroup
|
||||||
>
|
value={quality}
|
||||||
<RadioGroup
|
onValueChange={(value) => {
|
||||||
value={quality}
|
if (isExportQuality(value)) {
|
||||||
onValueChange={(value) => {
|
setQuality(value);
|
||||||
if (isExportQuality(value)) {
|
}
|
||||||
setQuality(value);
|
}}
|
||||||
}
|
>
|
||||||
}}
|
<div className="flex items-center space-x-2">
|
||||||
>
|
<RadioGroupItem value="low" id="low" />
|
||||||
|
<Label htmlFor="low">Low - Smallest file size</Label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<RadioGroupItem value="medium" id="medium" />
|
||||||
|
<Label htmlFor="medium">Medium - Balanced</Label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<RadioGroupItem value="high" id="high" />
|
||||||
|
<Label htmlFor="high">High - Recommended</Label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<RadioGroupItem value="very_high" id="very_high" />
|
||||||
|
<Label htmlFor="very_high">
|
||||||
|
Very High - Largest file size
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
</RadioGroup>
|
||||||
|
</SectionContent>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section collapsible defaultOpen={false}>
|
||||||
|
<SectionHeader title="Audio" />
|
||||||
|
<SectionContent>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<RadioGroupItem value="low" id="low" />
|
<Checkbox
|
||||||
<Label htmlFor="low">Low - Smallest file size</Label>
|
id="include-audio"
|
||||||
</div>
|
checked={includeAudio}
|
||||||
<div className="flex items-center space-x-2">
|
onCheckedChange={(checked) =>
|
||||||
<RadioGroupItem value="medium" id="medium" />
|
setIncludeAudio(!!checked)
|
||||||
<Label htmlFor="medium">Medium - Balanced</Label>
|
}
|
||||||
</div>
|
/>
|
||||||
<div className="flex items-center space-x-2">
|
<Label htmlFor="include-audio">
|
||||||
<RadioGroupItem value="high" id="high" />
|
Include audio in export
|
||||||
<Label htmlFor="high">High - Recommended</Label>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<RadioGroupItem value="very_high" id="very_high" />
|
|
||||||
<Label htmlFor="very_high">
|
|
||||||
Very High - Largest file size
|
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
</RadioGroup>
|
</SectionContent>
|
||||||
</PropertyGroup>
|
</Section>
|
||||||
|
|
||||||
<PropertyGroup
|
|
||||||
title="Audio"
|
|
||||||
titleClassName="text-sm"
|
|
||||||
defaultExpanded={false}
|
|
||||||
>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<Checkbox
|
|
||||||
id="include-audio"
|
|
||||||
checked={includeAudio}
|
|
||||||
onCheckedChange={(checked) =>
|
|
||||||
setIncludeAudio(!!checked)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Label htmlFor="include-audio">
|
|
||||||
Include audio in export
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
</PropertyGroup>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button onClick={handleExport} className="w-full gap-2">
|
<div className="p-3 pt-0">
|
||||||
<Download className="size-4" />
|
<Button onClick={handleExport} className="w-full gap-2">
|
||||||
Export
|
<Download className="size-4" />
|
||||||
</Button>
|
Export
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isExporting && (
|
{isExporting && (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4 p-3">
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<div className="flex items-center justify-between text-center">
|
<div className="flex items-center justify-between text-center">
|
||||||
<p className="text-muted-foreground mb-2 text-sm">
|
<p className="text-muted-foreground mb-2 text-sm">
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ export function DraggableItem({
|
|||||||
<AspectRatio
|
<AspectRatio
|
||||||
ratio={aspectRatio}
|
ratio={aspectRatio}
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-panel-accent relative overflow-hidden",
|
"bg-accent relative overflow-hidden",
|
||||||
isRounded && "rounded-sm",
|
isRounded && "rounded-sm",
|
||||||
isDraggable && "[&::-webkit-drag-ghost]:opacity-0",
|
isDraggable && "[&::-webkit-drag-ghost]:opacity-0",
|
||||||
)}
|
)}
|
||||||
@@ -163,7 +163,9 @@ export function DraggableItem({
|
|||||||
<div className="size-6 flex-shrink-0 overflow-hidden rounded-[0.35rem]">
|
<div className="size-6 flex-shrink-0 overflow-hidden rounded-[0.35rem]">
|
||||||
{preview}
|
{preview}
|
||||||
</div>
|
</div>
|
||||||
<span className="w-full flex-1 truncate text-sm text-left">{name}</span>
|
<span className="w-full flex-1 truncate text-sm text-left">
|
||||||
|
{name}
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -215,7 +217,7 @@ function PlusButton({
|
|||||||
<Button
|
<Button
|
||||||
size="icon"
|
size="icon"
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-background hover:bg-panel text-foreground absolute right-2 bottom-2 size-5",
|
"bg-background hover:bg-background text-foreground absolute right-2 bottom-2 size-5",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Separator } from "@/components/ui/separator";
|
|||||||
import { type Tab, useAssetsPanelStore } from "@/stores/assets-panel-store";
|
import { type Tab, useAssetsPanelStore } from "@/stores/assets-panel-store";
|
||||||
import { TabBar } from "./tabbar";
|
import { TabBar } from "./tabbar";
|
||||||
import { Captions } from "./views/captions";
|
import { Captions } from "./views/captions";
|
||||||
import { MediaView } from "./views/media";
|
import { MediaView } from "./views/assets";
|
||||||
import { SettingsView } from "./views/settings";
|
import { SettingsView } from "./views/settings";
|
||||||
import { SoundsView } from "./views/sounds";
|
import { SoundsView } from "./views/sounds";
|
||||||
import { StickersView } from "./views/stickers";
|
import { StickersView } from "./views/stickers";
|
||||||
@@ -43,7 +43,7 @@ export function AssetsPanel() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-panel flex h-full">
|
<div className="panel bg-background flex h-full rounded-sm border overflow-hidden">
|
||||||
<TabBar />
|
<TabBar />
|
||||||
<Separator orientation="vertical" />
|
<Separator orientation="vertical" />
|
||||||
<div className="flex-1 overflow-hidden">{viewMap[activeTab]}</div>
|
<div className="flex-1 overflow-hidden">{viewMap[activeTab]}</div>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
import { cn } from "@/utils/ui";
|
import { cn } from "@/utils/ui";
|
||||||
import {
|
import {
|
||||||
TAB_KEYS,
|
TAB_KEYS,
|
||||||
@@ -15,9 +16,9 @@ import {
|
|||||||
|
|
||||||
export function TabBar() {
|
export function TabBar() {
|
||||||
const { activeTab, setActiveTab } = useAssetsPanelStore();
|
const { activeTab, setActiveTab } = useAssetsPanelStore();
|
||||||
const scrollRef = useRef<HTMLDivElement>(null);
|
|
||||||
const [showTopFade, setShowTopFade] = useState(false);
|
const [showTopFade, setShowTopFade] = useState(false);
|
||||||
const [showBottomFade, setShowBottomFade] = useState(false);
|
const [showBottomFade, setShowBottomFade] = useState(false);
|
||||||
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const checkScrollPosition = useCallback(() => {
|
const checkScrollPosition = useCallback(() => {
|
||||||
const element = scrollRef.current;
|
const element = scrollRef.current;
|
||||||
@@ -48,32 +49,25 @@ export function TabBar() {
|
|||||||
<div className="relative flex">
|
<div className="relative flex">
|
||||||
<div
|
<div
|
||||||
ref={scrollRef}
|
ref={scrollRef}
|
||||||
className="scrollbar-hidden relative flex size-full flex-col items-center justify-start gap-5 overflow-y-auto px-4 py-4"
|
className="scrollbar-hidden relative flex size-full p-2 flex-col items-center justify-start gap-1.5 overflow-y-auto"
|
||||||
>
|
>
|
||||||
{TAB_KEYS.map((tabKey) => {
|
{TAB_KEYS.map((tabKey) => {
|
||||||
const tab = tabs[tabKey];
|
const tab = tabs[tabKey];
|
||||||
return (
|
return (
|
||||||
<Tooltip key={tabKey} delayDuration={10}>
|
<Tooltip key={tabKey} delayDuration={10}>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
variant={activeTab === tabKey ? "secondary" : "text"}
|
||||||
aria-label={tab.label}
|
aria-label={tab.label}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex cursor-pointer flex-col items-center gap-0.5 [&>svg]:size-4.5! opacity-100 hover:opacity-75",
|
"flex-col !p-1.5 !rounded-sm !h-auto [&_svg]:size-4.5",
|
||||||
activeTab === tabKey
|
activeTab !== tabKey &&
|
||||||
? "text-primary !opacity-100"
|
"border border-transparent text-muted-foreground",
|
||||||
: "text-muted-foreground",
|
|
||||||
)}
|
)}
|
||||||
onClick={() => setActiveTab(tabKey)}
|
onClick={() => setActiveTab(tabKey)}
|
||||||
onKeyDown={(event) => {
|
|
||||||
if (event.key === "Enter" || event.key === " ") {
|
|
||||||
event.preventDefault();
|
|
||||||
setActiveTab(tabKey);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<tab.icon className=" " />
|
<tab.icon />
|
||||||
</button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent
|
<TooltipContent
|
||||||
side="right"
|
side="right"
|
||||||
@@ -108,8 +102,8 @@ function FadeOverlay({
|
|||||||
className={cn(
|
className={cn(
|
||||||
"pointer-events-none absolute right-0 left-0 h-6",
|
"pointer-events-none absolute right-0 left-0 h-6",
|
||||||
direction === "top" && show
|
direction === "top" && show
|
||||||
? "from-panel top-0 bg-gradient-to-b to-transparent"
|
? "from-background top-0 bg-gradient-to-b to-transparent"
|
||||||
: "from-panel bottom-0 bg-gradient-to-t to-transparent",
|
: "from-background bottom-0 bg-gradient-to-t to-transparent",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import { PanelView } from "@/components/editor/panels/assets/views/base-view";
|
||||||
import { MediaDragOverlay } from "@/components/editor/panels/assets/drag-overlay";
|
import { MediaDragOverlay } from "@/components/editor/panels/assets/drag-overlay";
|
||||||
import { DraggableItem } from "@/components/editor/panels/assets/draggable-item";
|
import { DraggableItem } from "@/components/editor/panels/assets/draggable-item";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -29,14 +30,9 @@ import { useEditor } from "@/hooks/use-editor";
|
|||||||
import { useFileUpload } from "@/hooks/use-file-upload";
|
import { useFileUpload } from "@/hooks/use-file-upload";
|
||||||
import { useRevealItem } from "@/hooks/use-reveal-item";
|
import { useRevealItem } from "@/hooks/use-reveal-item";
|
||||||
import { processMediaAssets } from "@/lib/media/processing";
|
import { processMediaAssets } from "@/lib/media/processing";
|
||||||
import {
|
import { buildElementFromMedia } from "@/lib/timeline/element-utils";
|
||||||
buildImageElement,
|
|
||||||
buildUploadAudioElement,
|
|
||||||
buildVideoElement,
|
|
||||||
} from "@/lib/timeline/element-utils";
|
|
||||||
import { useAssetsPanelStore } from "@/stores/assets-panel-store";
|
import { useAssetsPanelStore } from "@/stores/assets-panel-store";
|
||||||
import type { MediaAsset } from "@/types/assets";
|
import type { MediaAsset } from "@/types/assets";
|
||||||
import type { CreateTimelineElement } from "@/types/timeline";
|
|
||||||
import { cn } from "@/utils/ui";
|
import { cn } from "@/utils/ui";
|
||||||
import {
|
import {
|
||||||
CloudUploadIcon,
|
CloudUploadIcon,
|
||||||
@@ -132,7 +128,15 @@ export function MediaView() {
|
|||||||
asset: MediaAsset;
|
asset: MediaAsset;
|
||||||
startTime: number;
|
startTime: number;
|
||||||
}): boolean => {
|
}): boolean => {
|
||||||
const element = createElementFromMedia({ asset, startTime });
|
const duration =
|
||||||
|
asset.duration ?? TIMELINE_CONSTANTS.DEFAULT_ELEMENT_DURATION;
|
||||||
|
const element = buildElementFromMedia({
|
||||||
|
mediaId: asset.id,
|
||||||
|
mediaType: asset.type,
|
||||||
|
name: asset.name,
|
||||||
|
duration,
|
||||||
|
startTime,
|
||||||
|
});
|
||||||
editor.timeline.insertElement({
|
editor.timeline.insertElement({
|
||||||
element,
|
element,
|
||||||
placement: { mode: "auto" },
|
placement: { mode: "auto" },
|
||||||
@@ -194,171 +198,166 @@ export function MediaView() {
|
|||||||
const renderCompactPreview = (item: MediaAsset) =>
|
const renderCompactPreview = (item: MediaAsset) =>
|
||||||
previewComponents.get(`compact-${item.id}`);
|
previewComponents.get(`compact-${item.id}`);
|
||||||
|
|
||||||
|
const mediaActions = (
|
||||||
|
<div>
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() =>
|
||||||
|
setMediaViewMode(mediaViewMode === "grid" ? "list" : "grid")
|
||||||
|
}
|
||||||
|
disabled={isProcessing}
|
||||||
|
className="items-center justify-center"
|
||||||
|
>
|
||||||
|
{mediaViewMode === "grid" ? (
|
||||||
|
<HugeiconsIcon icon={LeftToRightListDashIcon} />
|
||||||
|
) : (
|
||||||
|
<HugeiconsIcon icon={GridViewIcon} />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>
|
||||||
|
{mediaViewMode === "grid"
|
||||||
|
? "Switch to list view"
|
||||||
|
: "Switch to grid view"}
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
<Tooltip>
|
||||||
|
<DropdownMenu>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
variant="ghost"
|
||||||
|
disabled={isProcessing}
|
||||||
|
className="items-center justify-center"
|
||||||
|
>
|
||||||
|
<HugeiconsIcon icon={SortingOneNineIcon} />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<SortMenuItem
|
||||||
|
label="Name"
|
||||||
|
sortKey="name"
|
||||||
|
currentSortBy={sortBy}
|
||||||
|
currentSortOrder={sortOrder}
|
||||||
|
onSort={({ key }) => {
|
||||||
|
if (sortBy === key) {
|
||||||
|
setSortOrder(sortOrder === "asc" ? "desc" : "asc");
|
||||||
|
} else {
|
||||||
|
setSortBy(key);
|
||||||
|
setSortOrder("asc");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<SortMenuItem
|
||||||
|
label="Type"
|
||||||
|
sortKey="type"
|
||||||
|
currentSortBy={sortBy}
|
||||||
|
currentSortOrder={sortOrder}
|
||||||
|
onSort={({ key }) => {
|
||||||
|
if (sortBy === key) {
|
||||||
|
setSortOrder(sortOrder === "asc" ? "desc" : "asc");
|
||||||
|
} else {
|
||||||
|
setSortBy(key);
|
||||||
|
setSortOrder("asc");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<SortMenuItem
|
||||||
|
label="Duration"
|
||||||
|
sortKey="duration"
|
||||||
|
currentSortBy={sortBy}
|
||||||
|
currentSortOrder={sortOrder}
|
||||||
|
onSort={({ key }) => {
|
||||||
|
if (sortBy === key) {
|
||||||
|
setSortOrder(sortOrder === "asc" ? "desc" : "asc");
|
||||||
|
} else {
|
||||||
|
setSortBy(key);
|
||||||
|
setSortOrder("asc");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<SortMenuItem
|
||||||
|
label="File size"
|
||||||
|
sortKey="size"
|
||||||
|
currentSortBy={sortBy}
|
||||||
|
currentSortOrder={sortOrder}
|
||||||
|
onSort={({ key }) => {
|
||||||
|
if (sortBy === key) {
|
||||||
|
setSortOrder(sortOrder === "asc" ? "desc" : "asc");
|
||||||
|
} else {
|
||||||
|
setSortBy(key);
|
||||||
|
setSortOrder("asc");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>
|
||||||
|
Sort by {sortBy} (
|
||||||
|
{sortOrder === "asc" ? "ascending" : "descending"})
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={openFilePicker}
|
||||||
|
disabled={isProcessing}
|
||||||
|
size="sm"
|
||||||
|
className="items-center justify-center gap-1.5 ml-1.5"
|
||||||
|
>
|
||||||
|
<HugeiconsIcon icon={CloudUploadIcon} />
|
||||||
|
Import
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<input {...fileInputProps} />
|
<input {...fileInputProps} />
|
||||||
|
|
||||||
<div
|
<PanelView
|
||||||
className={`relative flex h-full flex-col gap-1 ${isDragOver ? "bg-accent/30" : ""}`}
|
title="Assets"
|
||||||
|
actions={mediaActions}
|
||||||
|
className={isDragOver ? "bg-accent/30" : ""}
|
||||||
{...dragProps}
|
{...dragProps}
|
||||||
>
|
>
|
||||||
<div className="bg-panel py-2 px-4 flex items-center justify-between border-b">
|
{isDragOver || filteredMediaItems.length === 0 ? (
|
||||||
<span className="text-muted-foreground text-sm">Assets</span>
|
<MediaDragOverlay
|
||||||
<div className="flex items-center gap-0">
|
isVisible={true}
|
||||||
<TooltipProvider>
|
isProcessing={isProcessing}
|
||||||
<Tooltip>
|
progress={progress}
|
||||||
<TooltipTrigger asChild>
|
onClick={openFilePicker}
|
||||||
<Button
|
/>
|
||||||
size="icon"
|
) : mediaViewMode === "grid" ? (
|
||||||
variant="text"
|
<GridView
|
||||||
onClick={() =>
|
items={filteredMediaItems}
|
||||||
setMediaViewMode(
|
renderPreview={renderPreview}
|
||||||
mediaViewMode === "grid" ? "list" : "grid",
|
onRemove={handleRemove}
|
||||||
)
|
onAddToTimeline={addElementAtTime}
|
||||||
}
|
highlightedId={highlightedId}
|
||||||
disabled={isProcessing}
|
registerElement={registerElement}
|
||||||
className="items-center justify-center"
|
/>
|
||||||
>
|
) : (
|
||||||
{mediaViewMode === "grid" ? (
|
<ListView
|
||||||
<HugeiconsIcon icon={LeftToRightListDashIcon} />
|
items={filteredMediaItems}
|
||||||
) : (
|
renderPreview={renderCompactPreview}
|
||||||
<HugeiconsIcon icon={GridViewIcon} />
|
onRemove={handleRemove}
|
||||||
)}
|
onAddToTimeline={addElementAtTime}
|
||||||
</Button>
|
highlightedId={highlightedId}
|
||||||
</TooltipTrigger>
|
registerElement={registerElement}
|
||||||
<TooltipContent>
|
/>
|
||||||
<p>
|
)}
|
||||||
{mediaViewMode === "grid"
|
</PanelView>
|
||||||
? "Switch to list view"
|
|
||||||
: "Switch to grid view"}
|
|
||||||
</p>
|
|
||||||
</TooltipContent>
|
|
||||||
<Tooltip>
|
|
||||||
<DropdownMenu>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button
|
|
||||||
size="icon"
|
|
||||||
variant="text"
|
|
||||||
disabled={isProcessing}
|
|
||||||
className="items-center justify-center"
|
|
||||||
>
|
|
||||||
<HugeiconsIcon icon={SortingOneNineIcon} />
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<DropdownMenuContent align="end">
|
|
||||||
<SortMenuItem
|
|
||||||
label="Name"
|
|
||||||
sortKey="name"
|
|
||||||
currentSortBy={sortBy}
|
|
||||||
currentSortOrder={sortOrder}
|
|
||||||
onSort={({ key }) => {
|
|
||||||
if (sortBy === key) {
|
|
||||||
setSortOrder(sortOrder === "asc" ? "desc" : "asc");
|
|
||||||
} else {
|
|
||||||
setSortBy(key);
|
|
||||||
setSortOrder("asc");
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<SortMenuItem
|
|
||||||
label="Type"
|
|
||||||
sortKey="type"
|
|
||||||
currentSortBy={sortBy}
|
|
||||||
currentSortOrder={sortOrder}
|
|
||||||
onSort={({ key }) => {
|
|
||||||
if (sortBy === key) {
|
|
||||||
setSortOrder(sortOrder === "asc" ? "desc" : "asc");
|
|
||||||
} else {
|
|
||||||
setSortBy(key);
|
|
||||||
setSortOrder("asc");
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<SortMenuItem
|
|
||||||
label="Duration"
|
|
||||||
sortKey="duration"
|
|
||||||
currentSortBy={sortBy}
|
|
||||||
currentSortOrder={sortOrder}
|
|
||||||
onSort={({ key }) => {
|
|
||||||
if (sortBy === key) {
|
|
||||||
setSortOrder(sortOrder === "asc" ? "desc" : "asc");
|
|
||||||
} else {
|
|
||||||
setSortBy(key);
|
|
||||||
setSortOrder("asc");
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<SortMenuItem
|
|
||||||
label="File size"
|
|
||||||
sortKey="size"
|
|
||||||
currentSortBy={sortBy}
|
|
||||||
currentSortOrder={sortOrder}
|
|
||||||
onSort={({ key }) => {
|
|
||||||
if (sortBy === key) {
|
|
||||||
setSortOrder(sortOrder === "asc" ? "desc" : "asc");
|
|
||||||
} else {
|
|
||||||
setSortBy(key);
|
|
||||||
setSortOrder("asc");
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>
|
|
||||||
Sort by {sortBy} (
|
|
||||||
{sortOrder === "asc" ? "ascending" : "descending"})
|
|
||||||
</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={openFilePicker}
|
|
||||||
disabled={isProcessing}
|
|
||||||
size="sm"
|
|
||||||
className="items-center justify-center gap-1.5 ml-1.5"
|
|
||||||
>
|
|
||||||
<HugeiconsIcon icon={CloudUploadIcon} />
|
|
||||||
Upload
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="scrollbar-thin size-full overflow-y-auto pt-1">
|
|
||||||
<div className="w-full flex-1 p-3 pt-0">
|
|
||||||
{isDragOver || filteredMediaItems.length === 0 ? (
|
|
||||||
<MediaDragOverlay
|
|
||||||
isVisible={true}
|
|
||||||
isProcessing={isProcessing}
|
|
||||||
progress={progress}
|
|
||||||
onClick={openFilePicker}
|
|
||||||
/>
|
|
||||||
) : mediaViewMode === "grid" ? (
|
|
||||||
<GridView
|
|
||||||
items={filteredMediaItems}
|
|
||||||
renderPreview={renderPreview}
|
|
||||||
onRemove={handleRemove}
|
|
||||||
onAddToTimeline={addElementAtTime}
|
|
||||||
highlightedId={highlightedId}
|
|
||||||
registerElement={registerElement}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<ListView
|
|
||||||
items={filteredMediaItems}
|
|
||||||
renderPreview={renderCompactPreview}
|
|
||||||
onRemove={handleRemove}
|
|
||||||
onAddToTimeline={addElementAtTime}
|
|
||||||
highlightedId={highlightedId}
|
|
||||||
registerElement={registerElement}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -636,40 +635,3 @@ function SortMenuItem({
|
|||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function createElementFromMedia({
|
|
||||||
asset,
|
|
||||||
startTime,
|
|
||||||
}: {
|
|
||||||
asset: MediaAsset;
|
|
||||||
startTime: number;
|
|
||||||
}): CreateTimelineElement {
|
|
||||||
const duration =
|
|
||||||
asset.duration ?? TIMELINE_CONSTANTS.DEFAULT_ELEMENT_DURATION;
|
|
||||||
|
|
||||||
switch (asset.type) {
|
|
||||||
case "video":
|
|
||||||
return buildVideoElement({
|
|
||||||
mediaId: asset.id,
|
|
||||||
name: asset.name,
|
|
||||||
duration,
|
|
||||||
startTime,
|
|
||||||
});
|
|
||||||
case "image":
|
|
||||||
return buildImageElement({
|
|
||||||
mediaId: asset.id,
|
|
||||||
name: asset.name,
|
|
||||||
duration,
|
|
||||||
startTime,
|
|
||||||
});
|
|
||||||
case "audio":
|
|
||||||
return buildUploadAudioElement({
|
|
||||||
mediaId: asset.id,
|
|
||||||
name: asset.name,
|
|
||||||
duration,
|
|
||||||
startTime,
|
|
||||||
});
|
|
||||||
default:
|
|
||||||
throw new Error(`Unsupported media type: ${asset.type}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { cn } from "@/utils/ui";
|
||||||
|
|
||||||
|
interface PanelViewProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
|
title?: string;
|
||||||
|
actions?: React.ReactNode;
|
||||||
|
children: React.ReactNode;
|
||||||
|
contentClassName?: string;
|
||||||
|
hideHeader?: boolean;
|
||||||
|
ref?: React.Ref<HTMLDivElement>;
|
||||||
|
onScroll?: React.UIEventHandler<HTMLDivElement>;
|
||||||
|
scrollRef?: React.Ref<HTMLDivElement>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PanelView({
|
||||||
|
title,
|
||||||
|
actions,
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
contentClassName,
|
||||||
|
hideHeader = false,
|
||||||
|
ref,
|
||||||
|
onScroll,
|
||||||
|
scrollRef,
|
||||||
|
...rest
|
||||||
|
}: PanelViewProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn("relative flex h-full flex-col", className)}
|
||||||
|
ref={ref}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
{!hideHeader && (
|
||||||
|
<div className="bg-background h-11 shrink-0 px-4 pr-2 flex items-center justify-between border-b">
|
||||||
|
<span className="text-muted-foreground text-sm">{title}</span>
|
||||||
|
{actions}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"scrollbar-thin size-full overflow-y-auto",
|
||||||
|
hideHeader ? "pt-4" : "pt-2",
|
||||||
|
)}
|
||||||
|
ref={scrollRef}
|
||||||
|
onScroll={onScroll}
|
||||||
|
>
|
||||||
|
<div className={cn("w-full flex-1 px-2 pt-0", contentClassName)}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { PropertyGroup } from "@/components/editor/panels/properties/property-item";
|
import { PanelView } from "@/components/editor/panels/assets/views/base-view";
|
||||||
import { PanelBaseView as BaseView } from "@/components/editor/panels/panel-base-view";
|
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@@ -21,6 +20,7 @@ import { transcriptionService } from "@/services/transcription/service";
|
|||||||
import { decodeAudioToFloat32 } from "@/lib/media/audio";
|
import { decodeAudioToFloat32 } from "@/lib/media/audio";
|
||||||
import { buildCaptionChunks } from "@/lib/transcription/caption";
|
import { buildCaptionChunks } from "@/lib/transcription/caption";
|
||||||
import { Spinner } from "@/components/ui/spinner";
|
import { Spinner } from "@/components/ui/spinner";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
|
||||||
export function Captions() {
|
export function Captions() {
|
||||||
const [selectedLanguage, setSelectedLanguage] =
|
const [selectedLanguage, setSelectedLanguage] =
|
||||||
@@ -108,16 +108,14 @@ export function Captions() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseView
|
<PanelView title="Captions" ref={containerRef}>
|
||||||
ref={containerRef}
|
<div className="flex flex-col gap-3">
|
||||||
className="flex h-full flex-col justify-between"
|
<Label>Language</Label>
|
||||||
>
|
|
||||||
<PropertyGroup title="Language">
|
|
||||||
<Select
|
<Select
|
||||||
value={selectedLanguage}
|
value={selectedLanguage}
|
||||||
onValueChange={(value) => handleLanguageChange({ value })}
|
onValueChange={(value) => handleLanguageChange({ value })}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="bg-panel-accent h-8 w-full text-xs">
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="Select a language" />
|
<SelectValue placeholder="Select a language" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@@ -129,7 +127,7 @@ export function Captions() {
|
|||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</PropertyGroup>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
{error && (
|
{error && (
|
||||||
@@ -147,6 +145,6 @@ export function Captions() {
|
|||||||
{isProcessing ? processingStep : "Generate transcript"}
|
{isProcessing ? processingStep : "Generate transcript"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</BaseView>
|
</PanelView>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,332 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import Image from "next/image";
|
||||||
|
import { memo, useCallback, useMemo } from "react";
|
||||||
|
import { PanelView } from "@/components/editor/panels/assets/views/base-view";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import {
|
||||||
|
BLUR_INTENSITY_PRESETS,
|
||||||
|
DEFAULT_BLUR_INTENSITY,
|
||||||
|
DEFAULT_COLOR,
|
||||||
|
FPS_PRESETS,
|
||||||
|
} from "@/constants/project-constants";
|
||||||
|
import { patternCraftGradients } from "@/data/colors/pattern-craft";
|
||||||
|
import { colors } from "@/data/colors/solid";
|
||||||
|
import { syntaxUIGradients } from "@/data/colors/syntax-ui";
|
||||||
|
import { useEditor } from "@/hooks/use-editor";
|
||||||
|
import { useEditorStore } from "@/stores/editor-store";
|
||||||
|
import { dimensionToAspectRatio } from "@/utils/geometry";
|
||||||
|
import { cn } from "@/utils/ui";
|
||||||
|
import {
|
||||||
|
PropertyGroup,
|
||||||
|
PropertyItem,
|
||||||
|
PropertyItemLabel,
|
||||||
|
PropertyItemValue,
|
||||||
|
} from "@/components/editor/panels/properties/section";
|
||||||
|
|
||||||
|
export function SettingsView() {
|
||||||
|
return (
|
||||||
|
<PanelView title="Project settings" contentClassName="px-0">
|
||||||
|
<div className="flex flex-col gap-8">
|
||||||
|
<ProjectInfoView />
|
||||||
|
<BackgroundView />
|
||||||
|
</div>
|
||||||
|
</PanelView>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ProjectInfoView() {
|
||||||
|
const editor = useEditor();
|
||||||
|
const activeProject = editor.project.getActive();
|
||||||
|
const { canvasPresets } = useEditorStore();
|
||||||
|
|
||||||
|
const findPresetIndexByAspectRatio = ({
|
||||||
|
presets,
|
||||||
|
targetAspectRatio,
|
||||||
|
}: {
|
||||||
|
presets: Array<{ width: number; height: number }>;
|
||||||
|
targetAspectRatio: string;
|
||||||
|
}) => {
|
||||||
|
for (let index = 0; index < presets.length; index++) {
|
||||||
|
const preset = presets[index];
|
||||||
|
const presetAspectRatio = dimensionToAspectRatio({
|
||||||
|
width: preset.width,
|
||||||
|
height: preset.height,
|
||||||
|
});
|
||||||
|
if (presetAspectRatio === targetAspectRatio) {
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
};
|
||||||
|
|
||||||
|
const currentCanvasSize = activeProject.settings.canvasSize;
|
||||||
|
const currentAspectRatio = dimensionToAspectRatio(currentCanvasSize);
|
||||||
|
const originalCanvasSize = activeProject.settings.originalCanvasSize ?? null;
|
||||||
|
const presetIndex = findPresetIndexByAspectRatio({
|
||||||
|
presets: canvasPresets,
|
||||||
|
targetAspectRatio: currentAspectRatio,
|
||||||
|
});
|
||||||
|
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) {
|
||||||
|
editor.project.updateSettings({ settings: { canvasSize: preset } });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFpsChange = (value: string) => {
|
||||||
|
const fps = parseFloat(value);
|
||||||
|
editor.project.updateSettings({ settings: { fps } });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<PropertyItem direction="column">
|
||||||
|
<PropertyItemLabel>Name</PropertyItemLabel>
|
||||||
|
<PropertyItemValue>{activeProject.metadata.name}</PropertyItemValue>
|
||||||
|
</PropertyItem>
|
||||||
|
|
||||||
|
<PropertyItem direction="column">
|
||||||
|
<PropertyItemLabel>Aspect ratio</PropertyItemLabel>
|
||||||
|
<PropertyItemValue>
|
||||||
|
<Select
|
||||||
|
value={selectedPresetValue}
|
||||||
|
onValueChange={(value) => handleAspectRatioChange({ value })}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select an aspect ratio" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value={originalPresetValue}>Original</SelectItem>
|
||||||
|
{canvasPresets.map((preset, index) => {
|
||||||
|
const label = dimensionToAspectRatio({
|
||||||
|
width: preset.width,
|
||||||
|
height: preset.height,
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<SelectItem key={label} value={index.toString()}>
|
||||||
|
{label}
|
||||||
|
</SelectItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</PropertyItemValue>
|
||||||
|
</PropertyItem>
|
||||||
|
|
||||||
|
<PropertyItem direction="column">
|
||||||
|
<PropertyItemLabel>Frame rate</PropertyItemLabel>
|
||||||
|
<PropertyItemValue>
|
||||||
|
<Select
|
||||||
|
value={activeProject.settings.fps.toString()}
|
||||||
|
onValueChange={handleFpsChange}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a frame rate" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{FPS_PRESETS.map((preset) => (
|
||||||
|
<SelectItem key={preset.value} value={preset.value}>
|
||||||
|
{preset.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</PropertyItemValue>
|
||||||
|
</PropertyItem>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const BlurPreview = memo(
|
||||||
|
({
|
||||||
|
blur,
|
||||||
|
isSelected,
|
||||||
|
onSelect,
|
||||||
|
}: {
|
||||||
|
blur: { label: string; value: number };
|
||||||
|
isSelected: boolean;
|
||||||
|
onSelect: () => void;
|
||||||
|
}) => (
|
||||||
|
<button
|
||||||
|
className={cn(
|
||||||
|
"border-foreground/15 hover:border-primary relative aspect-square size-20 cursor-pointer overflow-hidden rounded-sm border",
|
||||||
|
isSelected && "border-primary border-2",
|
||||||
|
)}
|
||||||
|
onClick={onSelect}
|
||||||
|
type="button"
|
||||||
|
aria-label={`Select ${blur.label} blur`}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src="https://images.unsplash.com/photo-1501785888041-af3ef285b470?q=80&w=1470&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
|
||||||
|
alt={`Blur preview ${blur.label}`}
|
||||||
|
fill
|
||||||
|
className="object-cover"
|
||||||
|
style={{ filter: `blur(${blur.value}px)` }}
|
||||||
|
loading="eager"
|
||||||
|
/>
|
||||||
|
<div className="absolute right-1 bottom-1 left-1 text-center">
|
||||||
|
<span className="rounded bg-black/50 px-1 text-xs text-white">
|
||||||
|
{blur.label}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
BlurPreview.displayName = "BlurPreview";
|
||||||
|
|
||||||
|
const BackgroundPreviews = memo(
|
||||||
|
({
|
||||||
|
backgrounds,
|
||||||
|
currentBackgroundColor,
|
||||||
|
isColorBackground,
|
||||||
|
handleColorSelect,
|
||||||
|
useBackgroundColor = false,
|
||||||
|
}: {
|
||||||
|
backgrounds: string[];
|
||||||
|
currentBackgroundColor: string;
|
||||||
|
isColorBackground: boolean;
|
||||||
|
handleColorSelect: ({ bg }: { bg: string }) => void;
|
||||||
|
useBackgroundColor?: boolean;
|
||||||
|
}) => {
|
||||||
|
return useMemo(
|
||||||
|
() =>
|
||||||
|
backgrounds.map((bg, index) => (
|
||||||
|
<button
|
||||||
|
key={`${index}-${bg}`}
|
||||||
|
className={cn(
|
||||||
|
"border-foreground/15 hover:border-primary aspect-square size-20 cursor-pointer rounded-sm border",
|
||||||
|
isColorBackground &&
|
||||||
|
bg === currentBackgroundColor &&
|
||||||
|
"border-primary border-2",
|
||||||
|
)}
|
||||||
|
style={
|
||||||
|
useBackgroundColor
|
||||||
|
? { backgroundColor: bg }
|
||||||
|
: {
|
||||||
|
background: bg,
|
||||||
|
backgroundSize: "cover",
|
||||||
|
backgroundPosition: "center",
|
||||||
|
backgroundRepeat: "no-repeat",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onClick={() => handleColorSelect({ bg })}
|
||||||
|
type="button"
|
||||||
|
aria-label={`Select background ${useBackgroundColor ? bg : index + 1}`}
|
||||||
|
/>
|
||||||
|
)),
|
||||||
|
[
|
||||||
|
backgrounds,
|
||||||
|
isColorBackground,
|
||||||
|
currentBackgroundColor,
|
||||||
|
handleColorSelect,
|
||||||
|
useBackgroundColor,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
BackgroundPreviews.displayName = "BackgroundPreviews";
|
||||||
|
|
||||||
|
function BackgroundView() {
|
||||||
|
const editor = useEditor();
|
||||||
|
const activeProject = editor.project.getActive();
|
||||||
|
const blurLevels = useMemo(() => BLUR_INTENSITY_PRESETS, []);
|
||||||
|
|
||||||
|
const handleBlurSelect = useCallback(
|
||||||
|
async ({ blurIntensity }: { blurIntensity: number }) => {
|
||||||
|
await editor.project.updateSettings({
|
||||||
|
settings: { background: { type: "blur", blurIntensity } },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[editor.project],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleColorSelect = useCallback(
|
||||||
|
async ({ color }: { color: string }) => {
|
||||||
|
await editor.project.updateSettings({
|
||||||
|
settings: { background: { type: "color", color } },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[editor.project],
|
||||||
|
);
|
||||||
|
|
||||||
|
const currentBlurIntensity =
|
||||||
|
activeProject.settings.background.type === "blur"
|
||||||
|
? activeProject.settings.background.blurIntensity
|
||||||
|
: DEFAULT_BLUR_INTENSITY;
|
||||||
|
|
||||||
|
const currentBackgroundColor =
|
||||||
|
activeProject.settings.background.type === "color"
|
||||||
|
? activeProject.settings.background.color
|
||||||
|
: DEFAULT_COLOR;
|
||||||
|
|
||||||
|
const isBlurBackground = activeProject.settings.background.type === "blur";
|
||||||
|
const isColorBackground = activeProject.settings.background.type === "color";
|
||||||
|
|
||||||
|
const blurPreviews = useMemo(
|
||||||
|
() =>
|
||||||
|
blurLevels.map((blur) => (
|
||||||
|
<BlurPreview
|
||||||
|
key={blur.value}
|
||||||
|
blur={blur}
|
||||||
|
isSelected={isBlurBackground && currentBlurIntensity === blur.value}
|
||||||
|
onSelect={() => handleBlurSelect({ blurIntensity: blur.value })}
|
||||||
|
/>
|
||||||
|
)),
|
||||||
|
[blurLevels, isBlurBackground, currentBlurIntensity, handleBlurSelect],
|
||||||
|
);
|
||||||
|
|
||||||
|
const backgroundSections = [
|
||||||
|
{ title: "Colors", backgrounds: colors, useBackgroundColor: true },
|
||||||
|
{ title: "Pattern craft", backgrounds: patternCraftGradients },
|
||||||
|
{ title: "Syntax UI", backgrounds: syntaxUIGradients },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<PropertyGroup title="Blur" hasBorderTop={false} defaultExpanded={false}>
|
||||||
|
<div className="flex flex-wrap gap-2">{blurPreviews}</div>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
{backgroundSections.map((section) => (
|
||||||
|
<PropertyGroup
|
||||||
|
key={section.title}
|
||||||
|
title={section.title}
|
||||||
|
defaultExpanded={false}
|
||||||
|
>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<BackgroundPreviews
|
||||||
|
backgrounds={section.backgrounds}
|
||||||
|
currentBackgroundColor={currentBackgroundColor}
|
||||||
|
isColorBackground={isColorBackground}
|
||||||
|
handleColorSelect={({ bg }) => handleColorSelect({ color: bg })}
|
||||||
|
useBackgroundColor={section.useBackgroundColor}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</PropertyGroup>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import Image from "next/image";
|
import { PanelView } from "@/components/editor/panels/assets/views/base-view";
|
||||||
import { memo, useCallback, useMemo } from "react";
|
|
||||||
import { PanelBaseView as BaseView } from "@/components/editor/panels/panel-base-view";
|
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@@ -10,89 +8,54 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import {
|
import { FPS_PRESETS } from "@/constants/project-constants";
|
||||||
BLUR_INTENSITY_PRESETS,
|
|
||||||
DEFAULT_BLUR_INTENSITY,
|
|
||||||
DEFAULT_COLOR,
|
|
||||||
FPS_PRESETS,
|
|
||||||
} from "@/constants/project-constants";
|
|
||||||
import { patternCraftGradients } from "@/data/colors/pattern-craft";
|
|
||||||
import { colors } from "@/data/colors/solid";
|
|
||||||
import { syntaxUIGradients } from "@/data/colors/syntax-ui";
|
|
||||||
import { useEditor } from "@/hooks/use-editor";
|
import { useEditor } from "@/hooks/use-editor";
|
||||||
import { useEditorStore } from "@/stores/editor-store";
|
import { useEditorStore } from "@/stores/editor-store";
|
||||||
import type { TProject } from "@/types/project";
|
|
||||||
import { dimensionToAspectRatio } from "@/utils/geometry";
|
import { dimensionToAspectRatio } from "@/utils/geometry";
|
||||||
import { cn } from "@/utils/ui";
|
|
||||||
import {
|
import {
|
||||||
PropertyGroup,
|
Section,
|
||||||
PropertyItem,
|
SectionContent,
|
||||||
PropertyItemLabel,
|
SectionHeader,
|
||||||
PropertyItemValue,
|
} from "@/components/editor/panels/properties/section";
|
||||||
} from "@/components/editor/panels/properties/property-item";
|
import { Label } from "@/components/ui/label";
|
||||||
import { HugeiconsIcon } from "@hugeicons/react";
|
import {
|
||||||
import { DropperIcon } from "@hugeicons/core-free-icons";
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from "@/components/ui/popover";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
export function SettingsView() {
|
export function SettingsView() {
|
||||||
return <ProjectSettingsTabs />;
|
const [open, setOpen] = useState(false);
|
||||||
}
|
|
||||||
|
|
||||||
function ProjectSettingsTabs() {
|
|
||||||
return (
|
return (
|
||||||
<BaseView
|
<PanelView contentClassName="px-0" hideHeader>
|
||||||
defaultTab="project-info"
|
<div className="flex flex-col">
|
||||||
tabs={[
|
<Section hasBorderTop={false}>
|
||||||
{
|
<SectionContent>
|
||||||
value: "project-info",
|
<ProjectInfoContent />
|
||||||
label: "Project info",
|
</SectionContent>
|
||||||
content: (
|
</Section>
|
||||||
<div className="p-5">
|
<Popover open={open} onOpenChange={setOpen}>
|
||||||
<ProjectInfoView />
|
<Section className="cursor-pointer">
|
||||||
</div>
|
<PopoverTrigger asChild>
|
||||||
),
|
<div>
|
||||||
},
|
<SectionHeader title="Background">
|
||||||
{
|
<div className="size-4 rounded-sm bg-red-500" />
|
||||||
value: "background",
|
</SectionHeader>
|
||||||
label: "Background",
|
|
||||||
content: (
|
|
||||||
<div className="flex h-full flex-col justify-between">
|
|
||||||
<div className="flex-1 p-5">
|
|
||||||
<BackgroundView />
|
|
||||||
</div>
|
</div>
|
||||||
{/* <div className="bg-panel/85 sticky -bottom-0 flex flex-col backdrop-blur-lg">
|
</PopoverTrigger>
|
||||||
<Separator />
|
</Section>
|
||||||
<Button className="text-muted-foreground hover:text-foreground/85 h-auto w-fit !bg-transparent p-5 py-4 text-xs shadow-none">
|
<PopoverContent>
|
||||||
Custom background
|
<div className="size-4 rounded-sm bg-red-500" />
|
||||||
<PlusIcon />
|
</PopoverContent>
|
||||||
</Button>
|
</Popover>
|
||||||
</div> */}
|
</div>
|
||||||
|
</PanelView>
|
||||||
{/* another ui */}
|
|
||||||
{/* <div className="flex flex-col justify-center items-center pb-5 sticky bottom-0">
|
|
||||||
<Button className="w-fit h-auto gap-1.5 px-3.5 py-1.5 bg-foreground hover:bg-foreground/85 text-background rounded-full">
|
|
||||||
<span className="text-sm">Custom</span>
|
|
||||||
<PlusIcon className="" />
|
|
||||||
</Button>
|
|
||||||
</div> */}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
className="flex h-full flex-col justify-between p-0"
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCurrentCanvasSize({ activeProject }: { activeProject: TProject }) {
|
function ProjectInfoContent() {
|
||||||
const { canvasSize } = activeProject.settings;
|
|
||||||
|
|
||||||
return {
|
|
||||||
width: canvasSize.width,
|
|
||||||
height: canvasSize.height,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function ProjectInfoView() {
|
|
||||||
const editor = useEditor();
|
const editor = useEditor();
|
||||||
const activeProject = editor.project.getActive();
|
const activeProject = editor.project.getActive();
|
||||||
const { canvasPresets } = useEditorStore();
|
const { canvasPresets } = useEditorStore();
|
||||||
@@ -117,7 +80,7 @@ function ProjectInfoView() {
|
|||||||
return -1;
|
return -1;
|
||||||
};
|
};
|
||||||
|
|
||||||
const currentCanvasSize = getCurrentCanvasSize({ activeProject });
|
const currentCanvasSize = activeProject.settings.canvasSize;
|
||||||
const currentAspectRatio = dimensionToAspectRatio(currentCanvasSize);
|
const currentAspectRatio = dimensionToAspectRatio(currentCanvasSize);
|
||||||
const originalCanvasSize = activeProject.settings.originalCanvasSize ?? null;
|
const originalCanvasSize = activeProject.settings.originalCanvasSize ?? null;
|
||||||
const presetIndex = findPresetIndexByAspectRatio({
|
const presetIndex = findPresetIndexByAspectRatio({
|
||||||
@@ -150,245 +113,55 @@ function ProjectInfoView() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<PropertyItem direction="column">
|
<div className="flex flex-col gap-2">
|
||||||
<PropertyItemLabel>Name</PropertyItemLabel>
|
<Label>Name</Label>
|
||||||
<PropertyItemValue>{activeProject.metadata.name}</PropertyItemValue>
|
<span className="leading-none text-sm">
|
||||||
</PropertyItem>
|
{activeProject.metadata.name}
|
||||||
|
|
||||||
<PropertyItem direction="column">
|
|
||||||
<PropertyItemLabel>Aspect ratio</PropertyItemLabel>
|
|
||||||
<PropertyItemValue>
|
|
||||||
<Select
|
|
||||||
value={selectedPresetValue}
|
|
||||||
onValueChange={(value) => handleAspectRatioChange({ value })}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="bg-panel-accent">
|
|
||||||
<SelectValue placeholder="Select an aspect ratio" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value={originalPresetValue}>Original</SelectItem>
|
|
||||||
{canvasPresets.map((preset, index) => {
|
|
||||||
const label = dimensionToAspectRatio({
|
|
||||||
width: preset.width,
|
|
||||||
height: preset.height,
|
|
||||||
});
|
|
||||||
return (
|
|
||||||
<SelectItem key={label} value={index.toString()}>
|
|
||||||
{label}
|
|
||||||
</SelectItem>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</PropertyItemValue>
|
|
||||||
</PropertyItem>
|
|
||||||
|
|
||||||
<PropertyItem direction="column">
|
|
||||||
<PropertyItemLabel>Frame rate</PropertyItemLabel>
|
|
||||||
<PropertyItemValue>
|
|
||||||
<Select
|
|
||||||
value={activeProject.settings.fps.toString()}
|
|
||||||
onValueChange={handleFpsChange}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="bg-panel-accent">
|
|
||||||
<SelectValue placeholder="Select a frame rate" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{FPS_PRESETS.map((preset) => (
|
|
||||||
<SelectItem key={preset.value} value={preset.value}>
|
|
||||||
{preset.label}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</PropertyItemValue>
|
|
||||||
</PropertyItem>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const BlurPreview = memo(
|
|
||||||
({
|
|
||||||
blur,
|
|
||||||
isSelected,
|
|
||||||
onSelect,
|
|
||||||
}: {
|
|
||||||
blur: { label: string; value: number };
|
|
||||||
isSelected: boolean;
|
|
||||||
onSelect: () => void;
|
|
||||||
}) => (
|
|
||||||
<button
|
|
||||||
className={cn(
|
|
||||||
"border-foreground/15 hover:border-primary relative aspect-square w-full cursor-pointer overflow-hidden rounded-sm border",
|
|
||||||
isSelected && "border-primary border-2",
|
|
||||||
)}
|
|
||||||
onClick={onSelect}
|
|
||||||
type="button"
|
|
||||||
aria-label={`Select ${blur.label} blur`}
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
src="https://images.unsplash.com/photo-1501785888041-af3ef285b470?q=80&w=1470&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
|
|
||||||
alt={`Blur preview ${blur.label}`}
|
|
||||||
fill
|
|
||||||
className="object-cover"
|
|
||||||
style={{ filter: `blur(${blur.value}px)` }}
|
|
||||||
loading="eager"
|
|
||||||
/>
|
|
||||||
<div className="absolute right-1 bottom-1 left-1 text-center">
|
|
||||||
<span className="rounded bg-black/50 px-1 text-xs text-white">
|
|
||||||
{blur.label}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
<div className="flex flex-col gap-2">
|
||||||
),
|
<Label>Aspect ratio</Label>
|
||||||
);
|
<Select
|
||||||
|
value={selectedPresetValue}
|
||||||
BlurPreview.displayName = "BlurPreview";
|
onValueChange={(value) => handleAspectRatioChange({ value })}
|
||||||
|
>
|
||||||
const BackgroundPreviews = memo(
|
<SelectTrigger className="w-fit">
|
||||||
({
|
<SelectValue placeholder="Select an aspect ratio" />
|
||||||
backgrounds,
|
</SelectTrigger>
|
||||||
currentBackgroundColor,
|
<SelectContent>
|
||||||
isColorBackground,
|
<SelectItem value={originalPresetValue}>Original</SelectItem>
|
||||||
handleColorSelect,
|
{canvasPresets.map((preset, index) => {
|
||||||
useBackgroundColor = false,
|
const label = dimensionToAspectRatio({
|
||||||
}: {
|
width: preset.width,
|
||||||
backgrounds: string[];
|
height: preset.height,
|
||||||
currentBackgroundColor: string;
|
});
|
||||||
isColorBackground: boolean;
|
return (
|
||||||
handleColorSelect: ({ bg }: { bg: string }) => void;
|
<SelectItem key={label} value={index.toString()}>
|
||||||
useBackgroundColor?: boolean;
|
{label}
|
||||||
}) => {
|
</SelectItem>
|
||||||
return useMemo(
|
);
|
||||||
() =>
|
})}
|
||||||
backgrounds.map((bg, index) => (
|
</SelectContent>
|
||||||
<button
|
</Select>
|
||||||
key={`${index}-${bg}`}
|
</div>
|
||||||
className={cn(
|
<div className="flex flex-col gap-2">
|
||||||
"border-foreground/15 hover:border-primary aspect-square w-full cursor-pointer rounded-sm border",
|
<Label>Frame rate</Label>
|
||||||
isColorBackground &&
|
<Select
|
||||||
bg === currentBackgroundColor &&
|
value={activeProject.settings.fps.toString()}
|
||||||
"border-primary border-2",
|
onValueChange={handleFpsChange}
|
||||||
)}
|
>
|
||||||
style={
|
<SelectTrigger className="w-fit">
|
||||||
useBackgroundColor
|
<SelectValue placeholder="Select a frame rate" />
|
||||||
? { backgroundColor: bg }
|
</SelectTrigger>
|
||||||
: {
|
<SelectContent>
|
||||||
background: bg,
|
{FPS_PRESETS.map((preset) => (
|
||||||
backgroundSize: "cover",
|
<SelectItem key={preset.value} value={preset.value}>
|
||||||
backgroundPosition: "center",
|
{preset.label}
|
||||||
backgroundRepeat: "no-repeat",
|
</SelectItem>
|
||||||
}
|
))}
|
||||||
}
|
</SelectContent>
|
||||||
onClick={() => handleColorSelect({ bg })}
|
</Select>
|
||||||
type="button"
|
</div>
|
||||||
aria-label={`Select background ${useBackgroundColor ? bg : index + 1}`}
|
|
||||||
/>
|
|
||||||
)),
|
|
||||||
[
|
|
||||||
backgrounds,
|
|
||||||
isColorBackground,
|
|
||||||
currentBackgroundColor,
|
|
||||||
handleColorSelect,
|
|
||||||
useBackgroundColor,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
BackgroundPreviews.displayName = "BackgroundPreviews";
|
|
||||||
|
|
||||||
function BackgroundView() {
|
|
||||||
const editor = useEditor();
|
|
||||||
const activeProject = editor.project.getActive();
|
|
||||||
const blurLevels = useMemo(() => BLUR_INTENSITY_PRESETS, []);
|
|
||||||
|
|
||||||
const handleBlurSelect = useCallback(
|
|
||||||
async ({ blurIntensity }: { blurIntensity: number }) => {
|
|
||||||
await editor.project.updateSettings({
|
|
||||||
settings: { background: { type: "blur", blurIntensity } },
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[editor.project],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleColorSelect = useCallback(
|
|
||||||
async ({ color }: { color: string }) => {
|
|
||||||
await editor.project.updateSettings({
|
|
||||||
settings: { background: { type: "color", color } },
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[editor.project],
|
|
||||||
);
|
|
||||||
|
|
||||||
const currentBlurIntensity =
|
|
||||||
activeProject.settings.background.type === "blur"
|
|
||||||
? activeProject.settings.background.blurIntensity
|
|
||||||
: DEFAULT_BLUR_INTENSITY;
|
|
||||||
|
|
||||||
const currentBackgroundColor =
|
|
||||||
activeProject.settings.background.type === "color"
|
|
||||||
? activeProject.settings.background.color
|
|
||||||
: DEFAULT_COLOR;
|
|
||||||
|
|
||||||
const isBlurBackground = activeProject.settings.background.type === "blur";
|
|
||||||
const isColorBackground = activeProject.settings.background.type === "color";
|
|
||||||
|
|
||||||
const blurPreviews = useMemo(
|
|
||||||
() =>
|
|
||||||
blurLevels.map((blur) => (
|
|
||||||
<BlurPreview
|
|
||||||
key={blur.value}
|
|
||||||
blur={blur}
|
|
||||||
isSelected={isBlurBackground && currentBlurIntensity === blur.value}
|
|
||||||
onSelect={() => handleBlurSelect({ blurIntensity: blur.value })}
|
|
||||||
/>
|
|
||||||
)),
|
|
||||||
[blurLevels, isBlurBackground, currentBlurIntensity, handleBlurSelect],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex h-full flex-col gap-4">
|
|
||||||
<PropertyGroup title="Blur" defaultExpanded={false}>
|
|
||||||
<div className="grid w-full grid-cols-4 gap-2">{blurPreviews}</div>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<PropertyGroup title="Colors" defaultExpanded={false}>
|
|
||||||
<div className="grid w-full grid-cols-4 gap-2">
|
|
||||||
<div className="border-foreground/15 hover:border-primary flex aspect-square w-full cursor-pointer items-center justify-center rounded-sm border">
|
|
||||||
<HugeiconsIcon icon={DropperIcon} className="size-4" />
|
|
||||||
</div>
|
|
||||||
<BackgroundPreviews
|
|
||||||
backgrounds={colors}
|
|
||||||
currentBackgroundColor={currentBackgroundColor}
|
|
||||||
isColorBackground={isColorBackground}
|
|
||||||
handleColorSelect={({ bg }) => handleColorSelect({ color: bg })}
|
|
||||||
useBackgroundColor={true}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<PropertyGroup title="Pattern craft" defaultExpanded={false}>
|
|
||||||
<div className="grid w-full grid-cols-4 gap-2">
|
|
||||||
<BackgroundPreviews
|
|
||||||
backgrounds={patternCraftGradients}
|
|
||||||
currentBackgroundColor={currentBackgroundColor}
|
|
||||||
isColorBackground={isColorBackground}
|
|
||||||
handleColorSelect={({ bg }) => handleColorSelect({ color: bg })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<PropertyGroup title="Syntax UI" defaultExpanded={false}>
|
|
||||||
<div className="grid w-full grid-cols-4 gap-2">
|
|
||||||
<BackgroundPreviews
|
|
||||||
backgrounds={syntaxUIGradients}
|
|
||||||
currentBackgroundColor={currentBackgroundColor}
|
|
||||||
isColorBackground={isColorBackground}
|
|
||||||
handleColorSelect={({ bg }) => handleColorSelect({ color: bg })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</PropertyGroup>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -235,7 +235,7 @@ function SoundEffectsView() {
|
|||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Input
|
<Input
|
||||||
placeholder="Search sound effects"
|
placeholder="Search sound effects"
|
||||||
className="bg-panel-accent w-full"
|
className="w-full"
|
||||||
containerClassName="w-full"
|
containerClassName="w-full"
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={({ currentTarget }) =>
|
onChange={({ currentTarget }) =>
|
||||||
@@ -406,7 +406,7 @@ function SavedSoundsView() {
|
|||||||
|
|
||||||
if (savedSounds.length === 0) {
|
if (savedSounds.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="bg-panel flex h-full flex-col items-center justify-center gap-3 p-4">
|
<div className="bg-background flex h-full flex-col items-center justify-center gap-3 p-4">
|
||||||
<HugeiconsIcon
|
<HugeiconsIcon
|
||||||
icon={FavouriteIcon}
|
icon={FavouriteIcon}
|
||||||
className="text-muted-foreground size-10"
|
className="text-muted-foreground size-10"
|
||||||
|
|||||||
@@ -4,144 +4,104 @@ import Image from "next/image";
|
|||||||
import type { CSSProperties } from "react";
|
import type { CSSProperties } from "react";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import { PanelView } from "@/components/editor/panels/assets/views/base-view";
|
||||||
import { DraggableItem } from "@/components/editor/panels/assets/draggable-item";
|
import { DraggableItem } from "@/components/editor/panels/assets/draggable-item";
|
||||||
import { PanelBaseView as BaseView } from "@/components/editor/panels/panel-base-view";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { InputWithBack } from "@/components/ui/input-with-back";
|
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { STICKER_CATEGORIES } from "@/constants/stickers-constants";
|
|
||||||
import { useInfiniteScroll } from "@/hooks/use-infinite-scroll";
|
|
||||||
import {
|
import {
|
||||||
buildIconSvgUrl,
|
resolveStickerId,
|
||||||
getIconSvgUrl,
|
type StickerItem as StickerData,
|
||||||
ICONIFY_HOSTS,
|
} from "@/lib/stickers";
|
||||||
POPULAR_COLLECTIONS,
|
|
||||||
} from "@/lib/iconify-api";
|
|
||||||
import { useStickersStore } from "@/stores/stickers-store";
|
import { useStickersStore } from "@/stores/stickers-store";
|
||||||
import type { StickerCategory } from "@/types/stickers";
|
|
||||||
import { cn } from "@/utils/ui";
|
import { cn } from "@/utils/ui";
|
||||||
import {
|
import {
|
||||||
ArrowRightIcon,
|
|
||||||
HappyIcon,
|
HappyIcon,
|
||||||
ClockIcon,
|
ClockIcon,
|
||||||
LayoutGridIcon,
|
|
||||||
MultiplicationSignIcon,
|
MultiplicationSignIcon,
|
||||||
SparklesIcon,
|
Search01Icon,
|
||||||
HashtagIcon,
|
|
||||||
} from "@hugeicons/core-free-icons";
|
} from "@hugeicons/core-free-icons";
|
||||||
import { HugeiconsIcon } from "@hugeicons/react";
|
import { HugeiconsIcon } from "@hugeicons/react";
|
||||||
import { Spinner } from "@/components/ui/spinner";
|
import { Spinner } from "@/components/ui/spinner";
|
||||||
|
import {
|
||||||
function isStickerCategory(value: string): value is StickerCategory {
|
Select,
|
||||||
return STICKER_CATEGORIES.includes(value as StickerCategory);
|
SelectContent,
|
||||||
}
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { OcSlidersVerticalIcon } from "@opencut/ui/icons";
|
||||||
|
import type { StickerCategory } from "@/types/stickers";
|
||||||
|
import { STICKER_CATEGORIES } from "@/constants/sticker-constants";
|
||||||
|
import { parseStickerId } from "@/lib/stickers/sticker-id";
|
||||||
|
|
||||||
export function StickersView() {
|
export function StickersView() {
|
||||||
const { selectedCategory, setSelectedCategory } = useStickersStore();
|
const { selectedCategory, setSelectedCategory } = useStickersStore();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseView
|
<PanelView
|
||||||
value={selectedCategory}
|
title="Stickers"
|
||||||
onValueChange={(v) => {
|
actions={
|
||||||
if (isStickerCategory(v)) {
|
<div className="flex items-center">
|
||||||
setSelectedCategory({ category: v });
|
<Select
|
||||||
}
|
value={selectedCategory}
|
||||||
}}
|
onValueChange={(value: StickerCategory) =>
|
||||||
tabs={[
|
setSelectedCategory({ category: value })
|
||||||
{
|
}
|
||||||
value: "all",
|
>
|
||||||
label: "All",
|
<SelectTrigger variant="outline" size="sm" className="mr-1.5">
|
||||||
icon: <HugeiconsIcon icon={LayoutGridIcon} className="size-3" />,
|
<SelectValue placeholder="All" />
|
||||||
content: <StickersContentView category="all" />,
|
</SelectTrigger>
|
||||||
},
|
<SelectContent>
|
||||||
{
|
{Object.entries(STICKER_CATEGORIES).map(([category, label]) => (
|
||||||
value: "general",
|
<SelectItem key={category} value={category}>
|
||||||
label: "Icons",
|
{label}
|
||||||
icon: <HugeiconsIcon icon={SparklesIcon} className="size-3" />,
|
</SelectItem>
|
||||||
content: <StickersContentView category="general" />,
|
))}
|
||||||
},
|
</SelectContent>
|
||||||
{
|
</Select>
|
||||||
value: "brands",
|
|
||||||
label: "Brands",
|
<Button variant="ghost" size="icon">
|
||||||
icon: <HugeiconsIcon icon={HashtagIcon} className="size-3" />,
|
<HugeiconsIcon icon={Search01Icon} className="!size-3.5" />
|
||||||
content: <StickersContentView category="brands" />,
|
</Button>
|
||||||
},
|
|
||||||
{
|
<Button variant="ghost" size="icon">
|
||||||
value: "emoji",
|
<OcSlidersVerticalIcon className="!size-3.5" />
|
||||||
label: "Emoji",
|
</Button>
|
||||||
icon: <HugeiconsIcon icon={HappyIcon} className="size-3" />,
|
</div>
|
||||||
content: <StickersContentView category="emoji" />,
|
}
|
||||||
},
|
>
|
||||||
]}
|
<StickersContentView />
|
||||||
className="flex h-full flex-col overflow-hidden p-0"
|
</PanelView>
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function StickerGrid({
|
function StickerGrid({
|
||||||
icons,
|
items,
|
||||||
onAdd,
|
shouldCapSize = false,
|
||||||
addingSticker,
|
|
||||||
capSize = false,
|
|
||||||
}: {
|
}: {
|
||||||
icons: string[];
|
items: StickerData[];
|
||||||
onAdd: (iconName: string) => void;
|
shouldCapSize?: boolean;
|
||||||
addingSticker: string | null;
|
|
||||||
capSize?: boolean;
|
|
||||||
}) {
|
}) {
|
||||||
const gridStyle: CSSProperties & {
|
const gridStyle: CSSProperties & {
|
||||||
"--sticker-min": string;
|
"--sticker-min": string;
|
||||||
"--sticker-max"?: string;
|
"--sticker-max"?: string;
|
||||||
} = {
|
} = {
|
||||||
gridTemplateColumns: capSize
|
gridTemplateColumns: shouldCapSize
|
||||||
? "repeat(auto-fill, minmax(var(--sticker-min, 96px), var(--sticker-max, 160px)))"
|
? "repeat(auto-fill, minmax(var(--sticker-min, 96px), var(--sticker-max, 160px)))"
|
||||||
: "repeat(auto-fit, minmax(var(--sticker-min, 96px), 1fr))",
|
: "repeat(auto-fit, minmax(var(--sticker-min, 96px), 1fr))",
|
||||||
"--sticker-min": "96px",
|
"--sticker-min": "96px",
|
||||||
...(capSize ? { "--sticker-max": "160px" } : {}),
|
...(shouldCapSize ? { "--sticker-max": "160px" } : {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid gap-2" style={gridStyle}>
|
<div className="grid gap-2" style={gridStyle}>
|
||||||
{icons.map((iconName) => (
|
{items.map((item) => (
|
||||||
<StickerItem
|
<StickerItem key={item.id} item={item} shouldCapSize={shouldCapSize} />
|
||||||
key={iconName}
|
|
||||||
iconName={iconName}
|
|
||||||
onAdd={onAdd}
|
|
||||||
isAdding={addingSticker === iconName}
|
|
||||||
capSize={capSize}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function CollectionGrid({
|
|
||||||
collections,
|
|
||||||
onSelectCollection,
|
|
||||||
}: {
|
|
||||||
collections: Array<{
|
|
||||||
prefix: string;
|
|
||||||
name: string;
|
|
||||||
total: number;
|
|
||||||
category?: string;
|
|
||||||
}>;
|
|
||||||
onSelectCollection: ({ prefix }: { prefix: string }) => void;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div className="grid grid-cols-1 gap-2">
|
|
||||||
{collections.map((collection) => (
|
|
||||||
<CollectionItem
|
|
||||||
key={collection.prefix}
|
|
||||||
title={collection.name}
|
|
||||||
subtitle={`${collection.total.toLocaleString()} icons${collection.category ? ` • ${collection.category}` : ""}`}
|
|
||||||
onClick={() => onSelectCollection({ prefix: collection.prefix })}
|
|
||||||
/>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -149,7 +109,7 @@ function CollectionGrid({
|
|||||||
|
|
||||||
function EmptyView({ message }: { message: string }) {
|
function EmptyView({ message }: { message: string }) {
|
||||||
return (
|
return (
|
||||||
<div className="bg-panel flex h-full flex-col items-center justify-center gap-3 p-4">
|
<div className="bg-background flex h-full flex-col items-center justify-center gap-3 p-4">
|
||||||
<HugeiconsIcon
|
<HugeiconsIcon
|
||||||
icon={HappyIcon}
|
icon={HappyIcon}
|
||||||
className="text-muted-foreground size-10"
|
className="text-muted-foreground size-10"
|
||||||
@@ -162,372 +122,125 @@ function EmptyView({ message }: { message: string }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function StickersContentView({ category }: { category: StickerCategory }) {
|
function StickersContentView() {
|
||||||
const {
|
const {
|
||||||
searchQuery,
|
searchQuery,
|
||||||
selectedCollection,
|
|
||||||
viewMode,
|
viewMode,
|
||||||
collections,
|
|
||||||
currentCollection,
|
|
||||||
searchResults,
|
searchResults,
|
||||||
recentStickers,
|
recentStickers,
|
||||||
isLoadingCollections,
|
|
||||||
isLoadingCollection,
|
|
||||||
isSearching,
|
isSearching,
|
||||||
setSearchQuery,
|
|
||||||
setSelectedCollection,
|
|
||||||
loadCollections,
|
|
||||||
searchStickers,
|
|
||||||
addStickerToTimeline,
|
|
||||||
clearRecentStickers,
|
clearRecentStickers,
|
||||||
setSelectedCategory,
|
|
||||||
addingSticker,
|
|
||||||
} = useStickersStore();
|
} = useStickersStore();
|
||||||
|
|
||||||
const [localSearchQuery, setLocalSearchQuery] = useState(searchQuery);
|
const itemsToDisplay = useMemo(() => {
|
||||||
const [collectionsToShow, setCollectionsToShow] = useState(20);
|
if (viewMode === "search" && searchResults) {
|
||||||
const [showCollectionItems, setShowCollectionItems] = useState(false);
|
return searchResults.items;
|
||||||
|
|
||||||
const filteredCollections = useMemo(() => {
|
|
||||||
if (category === "all") {
|
|
||||||
return Object.entries(collections).map(([prefix, collection]) => ({
|
|
||||||
prefix,
|
|
||||||
name: collection.name,
|
|
||||||
total: collection.total,
|
|
||||||
category: collection.category,
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const collectionList =
|
return [];
|
||||||
POPULAR_COLLECTIONS[category as keyof typeof POPULAR_COLLECTIONS];
|
}, [viewMode, searchResults]);
|
||||||
if (!collectionList) return [];
|
|
||||||
|
|
||||||
return collectionList
|
const recentStickerItems = useMemo(() => {
|
||||||
.map((c) => {
|
const items: StickerData[] = [];
|
||||||
const collection = collections[c.prefix];
|
for (const stickerId of recentStickers) {
|
||||||
return collection
|
const recentStickerItem = toRecentStickerItem({ stickerId });
|
||||||
? {
|
if (recentStickerItem) {
|
||||||
prefix: c.prefix,
|
items.push(recentStickerItem);
|
||||||
name: c.name,
|
|
||||||
total: collection.total,
|
|
||||||
}
|
|
||||||
: null;
|
|
||||||
})
|
|
||||||
.filter(Boolean) as Array<{
|
|
||||||
prefix: string;
|
|
||||||
name: string;
|
|
||||||
total: number;
|
|
||||||
}>;
|
|
||||||
}, [collections, category]);
|
|
||||||
|
|
||||||
const { scrollAreaRef, handleScroll } = useInfiniteScroll({
|
|
||||||
onLoadMore: () => setCollectionsToShow((prev) => prev + 20),
|
|
||||||
hasMore: filteredCollections.length > collectionsToShow,
|
|
||||||
isLoading: isLoadingCollections,
|
|
||||||
enabled: viewMode === "browse" && !selectedCollection && category === "all",
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (Object.keys(collections).length === 0) {
|
|
||||||
loadCollections();
|
|
||||||
}
|
|
||||||
}, [collections, loadCollections]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
if (localSearchQuery !== searchQuery) {
|
|
||||||
setSearchQuery({ query: localSearchQuery });
|
|
||||||
if (localSearchQuery.trim()) {
|
|
||||||
searchStickers({ query: localSearchQuery });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, 500);
|
}
|
||||||
|
return items;
|
||||||
|
}, [recentStickers]);
|
||||||
|
|
||||||
return () => clearTimeout(timer);
|
return (
|
||||||
}, [localSearchQuery, searchQuery, searchStickers, setSearchQuery]);
|
<div className="flex h-full flex-col gap-4">
|
||||||
|
{recentStickerItems.length > 0 && viewMode === "browse" && (
|
||||||
|
<div className="flex h-full flex-col gap-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<HugeiconsIcon
|
||||||
|
icon={ClockIcon}
|
||||||
|
className="text-muted-foreground size-4"
|
||||||
|
/>
|
||||||
|
<span className="text-sm font-medium">Recent</span>
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={clearRecentStickers}
|
||||||
|
className="hover:bg-accent ml-auto flex size-5 items-center justify-center rounded p-0"
|
||||||
|
>
|
||||||
|
<HugeiconsIcon
|
||||||
|
icon={MultiplicationSignIcon}
|
||||||
|
className="text-muted-foreground size-3"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>Clear recent stickers</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
|
<StickerGrid items={recentStickerItems.slice(0, 12)} shouldCapSize />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
const handleAddSticker = async (iconName: string) => {
|
{viewMode === "search" && (
|
||||||
|
<div className="h-full">
|
||||||
|
{isSearching ? (
|
||||||
|
<div className="flex items-center justify-center py-8">
|
||||||
|
<Spinner className="text-muted-foreground size-6" />
|
||||||
|
</div>
|
||||||
|
) : searchResults?.items.length ? (
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-muted-foreground text-sm">
|
||||||
|
{searchResults.total} results
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<StickerGrid items={itemsToDisplay} shouldCapSize />
|
||||||
|
</div>
|
||||||
|
) : searchQuery ? (
|
||||||
|
<EmptyView message={`No stickers found for "${searchQuery}"`} />
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StickerItemProps {
|
||||||
|
item: StickerData;
|
||||||
|
shouldCapSize?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function StickerItem({ item, shouldCapSize = false }: StickerItemProps) {
|
||||||
|
const { addingSticker, addStickerToTimeline } = useStickersStore();
|
||||||
|
const isAdding = addingSticker === item.id;
|
||||||
|
const [hasImageError, setHasImageError] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!item.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setHasImageError(false);
|
||||||
|
}, [item.id]);
|
||||||
|
|
||||||
|
const displayName = item.name;
|
||||||
|
|
||||||
|
const handleAdd = async () => {
|
||||||
try {
|
try {
|
||||||
await addStickerToTimeline({ iconName });
|
await addStickerToTimeline({
|
||||||
|
stickerId: item.id,
|
||||||
|
name: item.name,
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to add sticker:", error);
|
console.error("Failed to add sticker:", error);
|
||||||
toast.error("Failed to add sticker to timeline");
|
toast.error("Failed to add sticker to timeline");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const iconsToDisplay = useMemo(() => {
|
const preview = hasImageError ? (
|
||||||
if (viewMode === "search" && searchResults) {
|
|
||||||
return searchResults.icons;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (viewMode === "collection" && currentCollection) {
|
|
||||||
const icons: string[] = [];
|
|
||||||
|
|
||||||
if (currentCollection.uncategorized) {
|
|
||||||
icons.push(
|
|
||||||
...currentCollection.uncategorized.map(
|
|
||||||
(name) => `${currentCollection.prefix}:${name}`,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentCollection.categories) {
|
|
||||||
Object.values(currentCollection.categories).forEach((categoryIcons) => {
|
|
||||||
icons.push(
|
|
||||||
...categoryIcons.map(
|
|
||||||
(name) => `${currentCollection.prefix}:${name}`,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return icons.slice(0, 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
return [];
|
|
||||||
}, [viewMode, searchResults, currentCollection]);
|
|
||||||
|
|
||||||
const isInCollection = viewMode === "collection" && !!selectedCollection;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isInCollection) {
|
|
||||||
setShowCollectionItems(false);
|
|
||||||
const timer = setTimeout(() => setShowCollectionItems(true), 350);
|
|
||||||
return () => clearTimeout(timer);
|
|
||||||
} else {
|
|
||||||
setShowCollectionItems(false);
|
|
||||||
}
|
|
||||||
}, [isInCollection]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mt-1 flex h-full flex-col gap-5 p-4">
|
|
||||||
<div className="space-y-3">
|
|
||||||
<InputWithBack
|
|
||||||
isExpanded={isInCollection}
|
|
||||||
setIsExpanded={(expanded) => {
|
|
||||||
if (!expanded && isInCollection) {
|
|
||||||
setSelectedCollection({ collection: null });
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
placeholder={
|
|
||||||
category === "all"
|
|
||||||
? "Search all stickers"
|
|
||||||
: category === "general"
|
|
||||||
? "Search icons"
|
|
||||||
: category === "brands"
|
|
||||||
? "Search brands"
|
|
||||||
: "Search Emojis"
|
|
||||||
}
|
|
||||||
value={localSearchQuery}
|
|
||||||
onChange={setLocalSearchQuery}
|
|
||||||
disableAnimation={true}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="relative h-full overflow-hidden">
|
|
||||||
<ScrollArea
|
|
||||||
className="h-full flex-1"
|
|
||||||
ref={scrollAreaRef}
|
|
||||||
onScrollCapture={handleScroll}
|
|
||||||
>
|
|
||||||
<div className="flex h-full flex-col gap-4">
|
|
||||||
{recentStickers.length > 0 && viewMode === "browse" && (
|
|
||||||
<div className="h-full">
|
|
||||||
<div className="mb-2 flex items-center gap-2">
|
|
||||||
<HugeiconsIcon
|
|
||||||
icon={ClockIcon}
|
|
||||||
className="text-muted-foreground size-4"
|
|
||||||
/>
|
|
||||||
<span className="text-sm font-medium">Recent</span>
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={clearRecentStickers}
|
|
||||||
className="hover:bg-accent ml-auto flex size-5 items-center justify-center rounded p-0"
|
|
||||||
>
|
|
||||||
<HugeiconsIcon
|
|
||||||
icon={MultiplicationSignIcon}
|
|
||||||
className="text-muted-foreground size-3"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>Clear recent stickers</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
</div>
|
|
||||||
<StickerGrid
|
|
||||||
icons={recentStickers.slice(0, 12)}
|
|
||||||
onAdd={handleAddSticker}
|
|
||||||
addingSticker={addingSticker}
|
|
||||||
capSize
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{viewMode === "collection" && selectedCollection && (
|
|
||||||
<div className="h-full">
|
|
||||||
{isLoadingCollection ? (
|
|
||||||
<div className="flex items-center justify-center py-8">
|
|
||||||
<Spinner className="text-muted-foreground size-6" />
|
|
||||||
</div>
|
|
||||||
) : showCollectionItems ? (
|
|
||||||
<StickerGrid
|
|
||||||
icons={iconsToDisplay}
|
|
||||||
onAdd={handleAddSticker}
|
|
||||||
addingSticker={addingSticker}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="flex items-center justify-center py-8">
|
|
||||||
<Spinner className="text-muted-foreground size-6" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{viewMode === "search" && (
|
|
||||||
<div className="h-full">
|
|
||||||
{isSearching ? (
|
|
||||||
<div className="flex items-center justify-center py-8">
|
|
||||||
<Spinner className="text-muted-foreground size-6" />
|
|
||||||
</div>
|
|
||||||
) : searchResults?.icons.length ? (
|
|
||||||
<>
|
|
||||||
<div className="mb-3 flex items-center justify-between">
|
|
||||||
<span className="text-muted-foreground text-sm">
|
|
||||||
{searchResults.total} results
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<StickerGrid
|
|
||||||
icons={iconsToDisplay}
|
|
||||||
onAdd={handleAddSticker}
|
|
||||||
addingSticker={addingSticker}
|
|
||||||
capSize
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
) : searchQuery ? (
|
|
||||||
<div className="flex flex-col items-center justify-center gap-3 py-8">
|
|
||||||
<EmptyView
|
|
||||||
message={`No stickers found for "${searchQuery}"`}
|
|
||||||
/>
|
|
||||||
{category !== "all" && (
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => {
|
|
||||||
const q = localSearchQuery || searchQuery;
|
|
||||||
if (q) {
|
|
||||||
setSearchQuery({ query: q });
|
|
||||||
}
|
|
||||||
setSelectedCategory({ category: "all" });
|
|
||||||
if (q) {
|
|
||||||
searchStickers({ query: q });
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Search in all icons
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{viewMode === "browse" && !selectedCollection && (
|
|
||||||
<div className="h-full space-y-4">
|
|
||||||
{isLoadingCollections ? (
|
|
||||||
<div className="flex items-center justify-center py-8">
|
|
||||||
<Spinner className="text-muted-foreground size-6" />
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{category !== "all" && (
|
|
||||||
<div className="h-full">
|
|
||||||
<CollectionGrid
|
|
||||||
collections={filteredCollections}
|
|
||||||
onSelectCollection={({ prefix }) =>
|
|
||||||
setSelectedCollection({ collection: prefix })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{category === "all" && filteredCollections.length > 0 && (
|
|
||||||
<div className="h-full">
|
|
||||||
<CollectionGrid
|
|
||||||
collections={filteredCollections.slice(
|
|
||||||
0,
|
|
||||||
collectionsToShow,
|
|
||||||
)}
|
|
||||||
onSelectCollection={({ prefix }) =>
|
|
||||||
setSelectedCollection({ collection: prefix })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</ScrollArea>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CollectionItemProps {
|
|
||||||
title: string;
|
|
||||||
subtitle: string;
|
|
||||||
onClick: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function CollectionItem({ title, subtitle, onClick }: CollectionItemProps) {
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
className="h-auto justify-between rounded-md py-2"
|
|
||||||
onClick={onClick}
|
|
||||||
>
|
|
||||||
<div className="text-left">
|
|
||||||
<p className="font-medium">{title}</p>
|
|
||||||
<p className="text-muted-foreground text-xs">{subtitle}</p>
|
|
||||||
</div>
|
|
||||||
<HugeiconsIcon icon={ArrowRightIcon} className="size-4" />
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface StickerItemProps {
|
|
||||||
iconName: string;
|
|
||||||
onAdd: (iconName: string) => void;
|
|
||||||
isAdding?: boolean;
|
|
||||||
capSize?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
function StickerItem({
|
|
||||||
iconName,
|
|
||||||
onAdd,
|
|
||||||
isAdding,
|
|
||||||
capSize = false,
|
|
||||||
}: StickerItemProps) {
|
|
||||||
const [imageError, setImageError] = useState(false);
|
|
||||||
const [hostIndex, setHostIndex] = useState(0);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!iconName) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setImageError(false);
|
|
||||||
setHostIndex(0);
|
|
||||||
}, [iconName]);
|
|
||||||
|
|
||||||
const displayName = iconName.split(":")[1] || iconName;
|
|
||||||
const collectionPrefix = iconName.split(":")[0];
|
|
||||||
|
|
||||||
const preview = imageError ? (
|
|
||||||
<div className="flex size-full items-center justify-center p-2">
|
<div className="flex size-full items-center justify-center p-2">
|
||||||
<span className="text-muted-foreground text-center text-xs break-all">
|
<span className="text-muted-foreground text-center text-xs break-all">
|
||||||
{displayName}
|
{displayName}
|
||||||
@@ -536,21 +249,13 @@ function StickerItem({
|
|||||||
) : (
|
) : (
|
||||||
<div className="flex size-full items-center justify-center p-4">
|
<div className="flex size-full items-center justify-center p-4">
|
||||||
<Image
|
<Image
|
||||||
src={
|
src={item.previewUrl}
|
||||||
hostIndex === 0
|
|
||||||
? getIconSvgUrl(iconName, { width: 64, height: 64 })
|
|
||||||
: buildIconSvgUrl(
|
|
||||||
ICONIFY_HOSTS[Math.min(hostIndex, ICONIFY_HOSTS.length - 1)],
|
|
||||||
iconName,
|
|
||||||
{ width: 64, height: 64 },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
alt={displayName}
|
alt={displayName}
|
||||||
width={64}
|
width={64}
|
||||||
height={64}
|
height={64}
|
||||||
className="size-full object-contain"
|
className="size-full object-contain"
|
||||||
style={
|
style={
|
||||||
capSize
|
shouldCapSize
|
||||||
? {
|
? {
|
||||||
maxWidth: "var(--sticker-max, 160px)",
|
maxWidth: "var(--sticker-max, 160px)",
|
||||||
maxHeight: "var(--sticker-max, 160px)",
|
maxHeight: "var(--sticker-max, 160px)",
|
||||||
@@ -558,12 +263,7 @@ function StickerItem({
|
|||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
onError={() => {
|
onError={() => {
|
||||||
const next = hostIndex + 1;
|
setHasImageError(true);
|
||||||
if (next < ICONIFY_HOSTS.length) {
|
|
||||||
setHostIndex(next);
|
|
||||||
} else {
|
|
||||||
setImageError(true);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
unoptimized
|
unoptimized
|
||||||
@@ -572,44 +272,63 @@ function StickerItem({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip>
|
<div
|
||||||
<TooltipTrigger asChild>
|
className={cn("relative", isAdding && "pointer-events-none opacity-50")}
|
||||||
<div
|
>
|
||||||
className={cn(
|
<DraggableItem
|
||||||
"relative",
|
name={displayName}
|
||||||
isAdding && "pointer-events-none opacity-50",
|
preview={preview}
|
||||||
)}
|
dragData={{
|
||||||
>
|
id: item.id,
|
||||||
<DraggableItem
|
type: "sticker",
|
||||||
name={displayName}
|
name: displayName,
|
||||||
preview={preview}
|
stickerId: item.id,
|
||||||
dragData={{
|
}}
|
||||||
id: iconName,
|
onAddToTimeline={handleAdd}
|
||||||
type: "sticker",
|
aspectRatio={1}
|
||||||
name: displayName,
|
shouldShowLabel={false}
|
||||||
iconName,
|
isRounded
|
||||||
}}
|
variant="card"
|
||||||
onAddToTimeline={() => onAdd(iconName)}
|
containerClassName="w-full"
|
||||||
aspectRatio={1}
|
/>
|
||||||
shouldShowLabel={false}
|
{isAdding && (
|
||||||
isRounded={true}
|
<div className="absolute inset-0 z-10 flex items-center justify-center rounded-md bg-black/60">
|
||||||
variant="card"
|
<Spinner className="size-6 text-white" />
|
||||||
className=""
|
|
||||||
containerClassName="w-full"
|
|
||||||
/>
|
|
||||||
{isAdding && (
|
|
||||||
<div className="absolute inset-0 z-10 flex items-center justify-center rounded-md bg-black/60">
|
|
||||||
<Spinner className="size-6 text-white" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</TooltipTrigger>
|
)}
|
||||||
<TooltipContent>
|
</div>
|
||||||
<div className="space-y-1">
|
|
||||||
<p className="font-medium">{displayName}</p>
|
|
||||||
<p className="text-muted-foreground text-xs">{collectionPrefix}</p>
|
|
||||||
</div>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getStickerNameFromId({ stickerId }: { stickerId: string }): string {
|
||||||
|
const stickerIdParts = stickerId.split(":");
|
||||||
|
if (stickerIdParts.length <= 1) {
|
||||||
|
return stickerId;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
stickerIdParts.slice(1).join(":").split(":").pop()?.replaceAll("-", " ") ??
|
||||||
|
stickerId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toRecentStickerItem({
|
||||||
|
stickerId,
|
||||||
|
}: {
|
||||||
|
stickerId: string;
|
||||||
|
}): StickerData | null {
|
||||||
|
try {
|
||||||
|
const { providerId } = parseStickerId({ stickerId });
|
||||||
|
return {
|
||||||
|
id: stickerId,
|
||||||
|
provider: providerId,
|
||||||
|
name: getStickerNameFromId({ stickerId }),
|
||||||
|
previewUrl: resolveStickerId({
|
||||||
|
stickerId,
|
||||||
|
options: { width: 64, height: 64 },
|
||||||
|
}),
|
||||||
|
metadata: {},
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { DraggableItem } from "@/components/editor/panels/assets/draggable-item";
|
import { DraggableItem } from "@/components/editor/panels/assets/draggable-item";
|
||||||
import { PanelBaseView as BaseView } from "@/components/editor/panels/panel-base-view";
|
import { PanelView } from "@/components/editor/panels/assets/views/base-view";
|
||||||
import { useEditor } from "@/hooks/use-editor";
|
import { useEditor } from "@/hooks/use-editor";
|
||||||
import { DEFAULT_TEXT_ELEMENT } from "@/constants/text-constants";
|
import { DEFAULT_TEXT_ELEMENT } from "@/constants/text-constants";
|
||||||
import { buildTextElement } from "@/lib/timeline/element-utils";
|
import { buildTextElement } from "@/lib/timeline/element-utils";
|
||||||
@@ -23,11 +23,11 @@ export function TextView() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseView>
|
<PanelView title="Text">
|
||||||
<DraggableItem
|
<DraggableItem
|
||||||
name="Default text"
|
name="Default text"
|
||||||
preview={
|
preview={
|
||||||
<div className="bg-panel-accent flex size-full items-center justify-center rounded">
|
<div className="bg-accent flex size-full items-center justify-center rounded">
|
||||||
<span className="text-xs select-none">Default text</span>
|
<span className="text-xs select-none">Default text</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -41,6 +41,6 @@ export function TextView() {
|
|||||||
onAddToTimeline={handleAddToTimeline}
|
onAddToTimeline={handleAddToTimeline}
|
||||||
shouldShowLabel={false}
|
shouldShowLabel={false}
|
||||||
/>
|
/>
|
||||||
</BaseView>
|
</PanelView>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,85 +0,0 @@
|
|||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
|
||||||
import { Separator } from "@/components/ui/separator";
|
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
||||||
import { cn } from "@/utils/ui";
|
|
||||||
|
|
||||||
interface PanelBaseViewProps {
|
|
||||||
children?: React.ReactNode;
|
|
||||||
defaultTab?: string;
|
|
||||||
value?: string;
|
|
||||||
onValueChange?: (value: string) => void;
|
|
||||||
tabs?: {
|
|
||||||
value: string;
|
|
||||||
label: string;
|
|
||||||
icon?: React.ReactNode;
|
|
||||||
content: React.ReactNode;
|
|
||||||
}[];
|
|
||||||
className?: string;
|
|
||||||
ref?: React.Ref<HTMLDivElement>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ViewContent({
|
|
||||||
children,
|
|
||||||
className,
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode;
|
|
||||||
className?: string;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<ScrollArea className="flex-1">
|
|
||||||
<div className={cn("p-5", className)}>{children}</div>
|
|
||||||
</ScrollArea>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function PanelBaseView({
|
|
||||||
children,
|
|
||||||
defaultTab,
|
|
||||||
value,
|
|
||||||
onValueChange,
|
|
||||||
tabs,
|
|
||||||
className = "",
|
|
||||||
ref,
|
|
||||||
}: PanelBaseViewProps) {
|
|
||||||
return (
|
|
||||||
<div className={cn("flex h-full flex-col", className)} ref={ref}>
|
|
||||||
{!tabs || tabs.length === 0 ? (
|
|
||||||
<ViewContent className={className}>{children}</ViewContent>
|
|
||||||
) : (
|
|
||||||
<Tabs
|
|
||||||
defaultValue={defaultTab}
|
|
||||||
value={value}
|
|
||||||
onValueChange={onValueChange}
|
|
||||||
className="flex h-full flex-col"
|
|
||||||
>
|
|
||||||
<div className="bg-panel sticky top-0 z-10">
|
|
||||||
<div className="px-3 pt-3 pb-0">
|
|
||||||
<TabsList>
|
|
||||||
{tabs.map((tab) => (
|
|
||||||
<TabsTrigger key={tab.value} value={tab.value}>
|
|
||||||
{tab.icon ? (
|
|
||||||
<span className="mr-1 inline-flex items-center">
|
|
||||||
{tab.icon}
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
{tab.label}
|
|
||||||
</TabsTrigger>
|
|
||||||
))}
|
|
||||||
</TabsList>
|
|
||||||
</div>
|
|
||||||
<Separator className="mt-3" />
|
|
||||||
</div>
|
|
||||||
{tabs.map((tab) => (
|
|
||||||
<TabsContent
|
|
||||||
key={tab.value}
|
|
||||||
value={tab.value}
|
|
||||||
className="mt-0 flex min-h-0 flex-1 flex-col"
|
|
||||||
>
|
|
||||||
<ViewContent className={className}>{tab.content}</ViewContent>
|
|
||||||
</TabsContent>
|
|
||||||
))}
|
|
||||||
</Tabs>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEditor } from "@/hooks/use-editor";
|
||||||
|
import { findCurrentScene } from "@/lib/scenes";
|
||||||
|
import { getBookmarksActiveAtTime } from "@/lib/timeline/bookmarks";
|
||||||
|
|
||||||
|
export function BookmarkNoteOverlay() {
|
||||||
|
const editor = useEditor();
|
||||||
|
const currentTime = editor.playback.getCurrentTime();
|
||||||
|
const activeProject = editor.project.getActive();
|
||||||
|
|
||||||
|
if (!activeProject) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeScene = findCurrentScene({
|
||||||
|
scenes: activeProject.scenes,
|
||||||
|
currentSceneId: activeProject.currentSceneId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!activeScene) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bookmarks = activeScene.bookmarks;
|
||||||
|
const activeBookmarks = getBookmarksActiveAtTime({
|
||||||
|
bookmarks,
|
||||||
|
time: currentTime,
|
||||||
|
});
|
||||||
|
const bookmarksWithNotes = activeBookmarks.filter(
|
||||||
|
(bookmark) => bookmark.note != null && bookmark.note.trim() !== "",
|
||||||
|
);
|
||||||
|
|
||||||
|
if (bookmarksWithNotes.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="pointer-events-none absolute top-2 left-2 flex flex-col gap-1.5"
|
||||||
|
aria-live="polite"
|
||||||
|
>
|
||||||
|
{bookmarksWithNotes.map((bookmark) => (
|
||||||
|
<div
|
||||||
|
key={bookmark.time}
|
||||||
|
className="flex max-w-[min(200px,50vw)] px-2.5 py-1.5 text-left text-white text-xs shadow-md backdrop-blur-sm"
|
||||||
|
style={{
|
||||||
|
backgroundColor: "rgb(0 0 0 / 0.5)",
|
||||||
|
borderLeft: bookmark.color
|
||||||
|
? `3px solid ${bookmark.color}`
|
||||||
|
: "3px solid var(--primary)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{bookmark.note}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
ContextMenuCheckboxItem,
|
||||||
|
ContextMenuContent,
|
||||||
|
ContextMenuItem,
|
||||||
|
} from "@/components/ui/context-menu";
|
||||||
|
import { usePreviewStore } from "@/stores/preview-store";
|
||||||
|
|
||||||
|
export function PreviewContextMenu({
|
||||||
|
onToggleFullscreen,
|
||||||
|
containerRef,
|
||||||
|
}: {
|
||||||
|
onToggleFullscreen: () => void;
|
||||||
|
containerRef: React.RefObject<HTMLElement | null>;
|
||||||
|
}) {
|
||||||
|
const { overlays, setOverlayVisibility } = usePreviewStore();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ContextMenuContent className="w-56" container={containerRef.current}>
|
||||||
|
<ContextMenuItem onClick={onToggleFullscreen} inset>
|
||||||
|
Full screen
|
||||||
|
</ContextMenuItem>
|
||||||
|
<ContextMenuCheckboxItem
|
||||||
|
checked={overlays.bookmarks}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
setOverlayVisibility({ overlay: "bookmarks", isVisible: !!checked })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Show bookmarks
|
||||||
|
</ContextMenuCheckboxItem>
|
||||||
|
<ContextMenuItem inset>Show grid</ContextMenuItem>
|
||||||
|
</ContextMenuContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,10 +4,18 @@ import { useCallback, useMemo, useRef } from "react";
|
|||||||
import useDeepCompareEffect from "use-deep-compare-effect";
|
import useDeepCompareEffect from "use-deep-compare-effect";
|
||||||
import { useEditor } from "@/hooks/use-editor";
|
import { useEditor } from "@/hooks/use-editor";
|
||||||
import { useRafLoop } from "@/hooks/use-raf-loop";
|
import { useRafLoop } from "@/hooks/use-raf-loop";
|
||||||
|
import { useContainerSize } from "@/hooks/use-container-size";
|
||||||
|
import { useFullscreen } from "@/hooks/use-fullscreen";
|
||||||
import { CanvasRenderer } from "@/services/renderer/canvas-renderer";
|
import { CanvasRenderer } from "@/services/renderer/canvas-renderer";
|
||||||
import type { RootNode } from "@/services/renderer/nodes/root-node";
|
import type { RootNode } from "@/services/renderer/nodes/root-node";
|
||||||
import { buildScene } from "@/services/renderer/scene-builder";
|
import { buildScene } from "@/services/renderer/scene-builder";
|
||||||
import { getLastFrameTime } from "@/lib/time";
|
import { getLastFrameTime } from "@/lib/time";
|
||||||
|
import { PreviewInteractionOverlay } from "./preview-interaction-overlay";
|
||||||
|
import { BookmarkNoteOverlay } from "./bookmark-note-overlay";
|
||||||
|
import { ContextMenu, ContextMenuTrigger } from "@/components/ui/context-menu";
|
||||||
|
import { usePreviewStore } from "@/stores/preview-store";
|
||||||
|
import { PreviewContextMenu } from "./context-menu";
|
||||||
|
import { PreviewToolbar } from "./toolbar";
|
||||||
|
|
||||||
function usePreviewSize() {
|
function usePreviewSize() {
|
||||||
const editor = useEditor();
|
const editor = useEditor();
|
||||||
@@ -19,6 +27,30 @@ function usePreviewSize() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function PreviewPanel() {
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const { isFullscreen, toggleFullscreen } = useFullscreen({ containerRef });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className="panel bg-background relative flex size-full min-h-0 min-w-0 flex-col rounded-sm border"
|
||||||
|
>
|
||||||
|
<div className="flex min-h-0 min-w-0 flex-1 items-center justify-center p-2 pb-0">
|
||||||
|
<PreviewCanvas
|
||||||
|
onToggleFullscreen={toggleFullscreen}
|
||||||
|
containerRef={containerRef}
|
||||||
|
/>
|
||||||
|
<RenderTreeController />
|
||||||
|
</div>
|
||||||
|
<PreviewToolbar
|
||||||
|
isFullscreen={isFullscreen}
|
||||||
|
onToggleFullscreen={toggleFullscreen}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function RenderTreeController() {
|
function RenderTreeController() {
|
||||||
const editor = useEditor();
|
const editor = useEditor();
|
||||||
const tracks = editor.timeline.getTracks();
|
const tracks = editor.timeline.getTracks();
|
||||||
@@ -45,38 +77,66 @@ function RenderTreeController() {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PreviewPanel() {
|
function PreviewCanvas({
|
||||||
return (
|
onToggleFullscreen,
|
||||||
<div className="bg-panel relative flex h-full min-h-0 w-full min-w-0 flex-col rounded-sm">
|
containerRef,
|
||||||
<div className="flex min-h-0 min-w-0 flex-1 items-center justify-center p-2">
|
}: {
|
||||||
<PreviewCanvas />
|
onToggleFullscreen: () => void;
|
||||||
<RenderTreeController />
|
containerRef: React.RefObject<HTMLElement | null>;
|
||||||
</div>
|
}) {
|
||||||
</div>
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
);
|
const outerContainerRef = useRef<HTMLDivElement>(null);
|
||||||
}
|
const canvasBoundsRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
function PreviewCanvas() {
|
|
||||||
const ref = useRef<HTMLCanvasElement>(null);
|
|
||||||
const lastFrameRef = useRef(-1);
|
const lastFrameRef = useRef(-1);
|
||||||
const lastSceneRef = useRef<RootNode | null>(null);
|
const lastSceneRef = useRef<RootNode | null>(null);
|
||||||
const renderingRef = useRef(false);
|
const renderingRef = useRef(false);
|
||||||
const { width, height } = usePreviewSize();
|
const { width: nativeWidth, height: nativeHeight } = usePreviewSize();
|
||||||
|
const containerSize = useContainerSize({ containerRef: outerContainerRef });
|
||||||
const editor = useEditor();
|
const editor = useEditor();
|
||||||
const activeProject = editor.project.getActive();
|
const activeProject = editor.project.getActive();
|
||||||
|
const { overlays } = usePreviewStore();
|
||||||
|
|
||||||
const renderer = useMemo(() => {
|
const renderer = useMemo(() => {
|
||||||
return new CanvasRenderer({
|
return new CanvasRenderer({
|
||||||
width,
|
width: nativeWidth,
|
||||||
height,
|
height: nativeHeight,
|
||||||
fps: activeProject.settings.fps,
|
fps: activeProject.settings.fps,
|
||||||
});
|
});
|
||||||
}, [width, height, activeProject.settings.fps]);
|
}, [nativeWidth, nativeHeight, activeProject.settings.fps]);
|
||||||
|
|
||||||
|
const displaySize = useMemo(() => {
|
||||||
|
if (
|
||||||
|
!nativeWidth ||
|
||||||
|
!nativeHeight ||
|
||||||
|
containerSize.width === 0 ||
|
||||||
|
containerSize.height === 0
|
||||||
|
) {
|
||||||
|
return { width: nativeWidth ?? 0, height: nativeHeight ?? 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const paddingBuffer = 4;
|
||||||
|
const availableWidth = containerSize.width - paddingBuffer;
|
||||||
|
const availableHeight = containerSize.height - paddingBuffer;
|
||||||
|
|
||||||
|
const aspectRatio = nativeWidth / nativeHeight;
|
||||||
|
const containerAspect = availableWidth / availableHeight;
|
||||||
|
|
||||||
|
const displayWidth =
|
||||||
|
containerAspect > aspectRatio
|
||||||
|
? availableHeight * aspectRatio
|
||||||
|
: availableWidth;
|
||||||
|
const displayHeight =
|
||||||
|
containerAspect > aspectRatio
|
||||||
|
? availableHeight
|
||||||
|
: availableWidth / aspectRatio;
|
||||||
|
|
||||||
|
return { width: displayWidth, height: displayHeight };
|
||||||
|
}, [nativeWidth, nativeHeight, containerSize.width, containerSize.height]);
|
||||||
|
|
||||||
const renderTree = editor.renderer.getRenderTree();
|
const renderTree = editor.renderer.getRenderTree();
|
||||||
|
|
||||||
const render = useCallback(() => {
|
const render = useCallback(() => {
|
||||||
if (ref.current && renderTree && !renderingRef.current) {
|
if (canvasRef.current && renderTree && !renderingRef.current) {
|
||||||
const time = editor.playback.getCurrentTime();
|
const time = editor.playback.getCurrentTime();
|
||||||
const lastFrameTime = getLastFrameTime({
|
const lastFrameTime = getLastFrameTime({
|
||||||
duration: renderTree.duration,
|
duration: renderTree.duration,
|
||||||
@@ -96,7 +156,7 @@ function PreviewCanvas() {
|
|||||||
.renderToCanvas({
|
.renderToCanvas({
|
||||||
node: renderTree,
|
node: renderTree,
|
||||||
time: renderTime,
|
time: renderTime,
|
||||||
targetCanvas: ref.current,
|
targetCanvas: canvasRef.current,
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
renderingRef.current = false;
|
renderingRef.current = false;
|
||||||
@@ -108,17 +168,43 @@ function PreviewCanvas() {
|
|||||||
useRafLoop(render);
|
useRafLoop(render);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<canvas
|
<div
|
||||||
ref={ref}
|
ref={outerContainerRef}
|
||||||
width={width}
|
className="relative flex size-full items-center justify-center"
|
||||||
height={height}
|
>
|
||||||
className="block max-h-full max-w-full border"
|
<ContextMenu>
|
||||||
style={{
|
<ContextMenuTrigger asChild>
|
||||||
background:
|
<div
|
||||||
activeProject.settings.background.type === "blur"
|
ref={canvasBoundsRef}
|
||||||
? "transparent"
|
className="relative"
|
||||||
: activeProject?.settings.background.color,
|
style={{ width: displaySize.width, height: displaySize.height }}
|
||||||
}}
|
>
|
||||||
/>
|
<canvas
|
||||||
|
ref={canvasRef}
|
||||||
|
width={nativeWidth}
|
||||||
|
height={nativeHeight}
|
||||||
|
className="block border"
|
||||||
|
style={{
|
||||||
|
width: displaySize.width,
|
||||||
|
height: displaySize.height,
|
||||||
|
background:
|
||||||
|
activeProject.settings.background.type === "blur"
|
||||||
|
? "transparent"
|
||||||
|
: activeProject?.settings.background.color,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<PreviewInteractionOverlay
|
||||||
|
canvasRef={canvasRef}
|
||||||
|
containerRef={canvasBoundsRef}
|
||||||
|
/>
|
||||||
|
{overlays.bookmarks && <BookmarkNoteOverlay />}
|
||||||
|
</div>
|
||||||
|
</ContextMenuTrigger>
|
||||||
|
<PreviewContextMenu
|
||||||
|
onToggleFullscreen={onToggleFullscreen}
|
||||||
|
containerRef={containerRef}
|
||||||
|
/>
|
||||||
|
</ContextMenu>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEditorStore } from "@/stores/editor-store";
|
import { usePreviewStore } from "@/stores/preview-store";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
|
||||||
function TikTokGuide() {
|
function TikTokGuide() {
|
||||||
@@ -18,7 +18,7 @@ function TikTokGuide() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function LayoutGuideOverlay() {
|
export function LayoutGuideOverlay() {
|
||||||
const { layoutGuide } = useEditorStore();
|
const { layoutGuide } = usePreviewStore();
|
||||||
|
|
||||||
if (layoutGuide.platform === null) return null;
|
if (layoutGuide.platform === null) return null;
|
||||||
if (layoutGuide.platform === "tiktok") return <TikTokGuide />;
|
if (layoutGuide.platform === "tiktok") return <TikTokGuide />;
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { usePreviewInteraction } from "@/hooks/use-preview-interaction";
|
||||||
|
import { TransformHandles } from "./transform-handles";
|
||||||
|
import { SnapGuides } from "./snap-guides";
|
||||||
|
import { TextEditOverlay } from "./text-edit-overlay";
|
||||||
|
|
||||||
|
export function PreviewInteractionOverlay({
|
||||||
|
canvasRef,
|
||||||
|
containerRef,
|
||||||
|
}: {
|
||||||
|
canvasRef: React.RefObject<HTMLCanvasElement | null>;
|
||||||
|
containerRef: React.RefObject<HTMLDivElement | null>;
|
||||||
|
}) {
|
||||||
|
const {
|
||||||
|
onPointerDown,
|
||||||
|
onPointerMove,
|
||||||
|
onPointerUp,
|
||||||
|
onDoubleClick,
|
||||||
|
snapLines,
|
||||||
|
editingText,
|
||||||
|
commitTextEdit,
|
||||||
|
cancelTextEdit,
|
||||||
|
} = usePreviewInteraction({ canvasRef });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="absolute inset-0">
|
||||||
|
{/* biome-ignore lint/a11y/noStaticElementInteractions: canvas overlay, pointer-only interaction */}
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 pointer-events-auto"
|
||||||
|
onPointerDown={onPointerDown}
|
||||||
|
onPointerMove={onPointerMove}
|
||||||
|
onPointerUp={onPointerUp}
|
||||||
|
onDoubleClick={onDoubleClick}
|
||||||
|
/>
|
||||||
|
{editingText ? (
|
||||||
|
<TextEditOverlay
|
||||||
|
canvasRef={canvasRef}
|
||||||
|
containerRef={containerRef}
|
||||||
|
trackId={editingText.trackId}
|
||||||
|
elementId={editingText.elementId}
|
||||||
|
element={editingText.element}
|
||||||
|
onCommit={commitTextEdit}
|
||||||
|
onCancel={cancelTextEdit}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<TransformHandles canvasRef={canvasRef} containerRef={containerRef} />
|
||||||
|
)}
|
||||||
|
<SnapGuides
|
||||||
|
lines={snapLines}
|
||||||
|
canvasRef={canvasRef}
|
||||||
|
containerRef={containerRef}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEditor } from "@/hooks/use-editor";
|
||||||
|
import type { SnapLine } from "@/lib/preview/preview-snap";
|
||||||
|
import { positionToOverlay } from "@/lib/preview/preview-coords";
|
||||||
|
|
||||||
|
export function SnapGuides({
|
||||||
|
lines,
|
||||||
|
canvasRef,
|
||||||
|
containerRef,
|
||||||
|
}: {
|
||||||
|
lines: SnapLine[];
|
||||||
|
canvasRef: React.RefObject<HTMLCanvasElement | null>;
|
||||||
|
containerRef: React.RefObject<HTMLDivElement | null>;
|
||||||
|
}) {
|
||||||
|
const editor = useEditor();
|
||||||
|
const canvasSize = editor.project.getActive().settings.canvasSize;
|
||||||
|
const canvasRect = canvasRef.current?.getBoundingClientRect();
|
||||||
|
const containerRect = containerRef.current?.getBoundingClientRect();
|
||||||
|
|
||||||
|
if (!canvasRect || !containerRect || lines.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const toOverlayX = (logicalX: number) =>
|
||||||
|
positionToOverlay({
|
||||||
|
positionX: logicalX,
|
||||||
|
positionY: 0,
|
||||||
|
canvasRect,
|
||||||
|
containerRect,
|
||||||
|
canvasSize,
|
||||||
|
}).x;
|
||||||
|
|
||||||
|
const toOverlayY = (logicalY: number) =>
|
||||||
|
positionToOverlay({
|
||||||
|
positionX: 0,
|
||||||
|
positionY: logicalY,
|
||||||
|
canvasRect,
|
||||||
|
containerRect,
|
||||||
|
canvasSize,
|
||||||
|
}).y;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="pointer-events-none absolute inset-0" aria-hidden>
|
||||||
|
{lines.map((line) => {
|
||||||
|
if (line.type === "vertical") {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={`vertical-${line.position}`}
|
||||||
|
className="absolute top-0 bottom-0 w-px bg-white/70"
|
||||||
|
style={{ left: toOverlayX(line.position) }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={`horizontal-${line.position}`}
|
||||||
|
className="absolute left-0 right-0 h-px bg-white/70"
|
||||||
|
style={{ top: toOverlayY(line.position) }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useCallback, useEffect, useRef } from "react";
|
||||||
|
import { useEditor } from "@/hooks/use-editor";
|
||||||
|
import type { TextElement } from "@/types/timeline";
|
||||||
|
import {
|
||||||
|
positionToOverlay,
|
||||||
|
getDisplayScale,
|
||||||
|
} from "@/lib/preview/preview-coords";
|
||||||
|
import {
|
||||||
|
DEFAULT_LINE_HEIGHT,
|
||||||
|
FONT_SIZE_SCALE_REFERENCE,
|
||||||
|
} from "@/constants/text-constants";
|
||||||
|
|
||||||
|
const TEXT_BACKGROUND_PADDING = "4px 8px";
|
||||||
|
const TEXT_EDIT_VERTICAL_OFFSET_EM = 0.06;
|
||||||
|
|
||||||
|
export function TextEditOverlay({
|
||||||
|
canvasRef,
|
||||||
|
containerRef,
|
||||||
|
trackId,
|
||||||
|
elementId,
|
||||||
|
element,
|
||||||
|
onCommit,
|
||||||
|
onCancel,
|
||||||
|
}: {
|
||||||
|
canvasRef: React.RefObject<HTMLCanvasElement | null>;
|
||||||
|
containerRef: React.RefObject<HTMLDivElement | null>;
|
||||||
|
trackId: string;
|
||||||
|
elementId: string;
|
||||||
|
element: TextElement;
|
||||||
|
onCommit: () => void;
|
||||||
|
onCancel: () => void;
|
||||||
|
}) {
|
||||||
|
const editor = useEditor();
|
||||||
|
const divRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const div = divRef.current;
|
||||||
|
if (!div) return;
|
||||||
|
div.focus();
|
||||||
|
const range = document.createRange();
|
||||||
|
range.selectNodeContents(div);
|
||||||
|
const selection = window.getSelection();
|
||||||
|
selection?.removeAllRanges();
|
||||||
|
selection?.addRange(range);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleInput = useCallback(() => {
|
||||||
|
const div = divRef.current;
|
||||||
|
if (!div) return;
|
||||||
|
const text = div.innerText;
|
||||||
|
editor.timeline.previewElements({
|
||||||
|
updates: [{ trackId, elementId, updates: { content: text } }],
|
||||||
|
});
|
||||||
|
}, [editor.timeline, trackId, elementId]);
|
||||||
|
|
||||||
|
const handleKeyDown = useCallback(
|
||||||
|
(event: React.KeyboardEvent) => {
|
||||||
|
const { key } = event;
|
||||||
|
if (key === "Escape") {
|
||||||
|
event.preventDefault();
|
||||||
|
onCancel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onCancel],
|
||||||
|
);
|
||||||
|
|
||||||
|
const canvasRect = canvasRef.current?.getBoundingClientRect();
|
||||||
|
const containerRect = containerRef.current?.getBoundingClientRect();
|
||||||
|
const canvasSize = editor.project.getActive().settings.canvasSize;
|
||||||
|
|
||||||
|
if (!canvasRect || !containerRect || !canvasSize) return null;
|
||||||
|
|
||||||
|
const { x: posX, y: posY } = positionToOverlay({
|
||||||
|
positionX: element.transform.position.x,
|
||||||
|
positionY: element.transform.position.y,
|
||||||
|
canvasRect,
|
||||||
|
containerRect,
|
||||||
|
canvasSize,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { x: displayScaleX } = getDisplayScale({
|
||||||
|
canvasRect,
|
||||||
|
canvasSize,
|
||||||
|
});
|
||||||
|
|
||||||
|
const displayFontSize =
|
||||||
|
element.fontSize *
|
||||||
|
(canvasSize.height / FONT_SIZE_SCALE_REFERENCE) *
|
||||||
|
displayScaleX;
|
||||||
|
|
||||||
|
const verticalAlignmentOffset =
|
||||||
|
displayFontSize * TEXT_EDIT_VERTICAL_OFFSET_EM;
|
||||||
|
|
||||||
|
const lineHeight = element.lineHeight ?? DEFAULT_LINE_HEIGHT;
|
||||||
|
const fontWeight = element.fontWeight === "bold" ? "bold" : "normal";
|
||||||
|
const fontStyle = element.fontStyle === "italic" ? "italic" : "normal";
|
||||||
|
const letterSpacing = element.letterSpacing ?? 0;
|
||||||
|
const backgroundColor =
|
||||||
|
element.backgroundColor === "transparent"
|
||||||
|
? "transparent"
|
||||||
|
: element.backgroundColor;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="absolute"
|
||||||
|
style={{
|
||||||
|
left: posX,
|
||||||
|
top: posY - verticalAlignmentOffset,
|
||||||
|
transform: `translate(-50%, -50%) scale(${element.transform.scale}) rotate(${element.transform.rotate}deg)`,
|
||||||
|
transformOrigin: "center center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* biome-ignore lint/a11y/useSemanticElements: contenteditable required for multiline, IME, paste */}
|
||||||
|
<div
|
||||||
|
ref={divRef}
|
||||||
|
contentEditable
|
||||||
|
suppressContentEditableWarning
|
||||||
|
tabIndex={0}
|
||||||
|
role="textbox"
|
||||||
|
aria-label="Edit text"
|
||||||
|
className="cursor-text select-text outline-none whitespace-pre"
|
||||||
|
style={{
|
||||||
|
fontSize: displayFontSize,
|
||||||
|
fontFamily: element.fontFamily,
|
||||||
|
fontWeight,
|
||||||
|
fontStyle,
|
||||||
|
textAlign: element.textAlign,
|
||||||
|
letterSpacing: `${letterSpacing}px`,
|
||||||
|
lineHeight,
|
||||||
|
color: element.color,
|
||||||
|
backgroundColor,
|
||||||
|
minHeight: displayFontSize * lineHeight,
|
||||||
|
textDecoration: element.textDecoration ?? "none",
|
||||||
|
padding:
|
||||||
|
backgroundColor === "transparent" ? 0 : TEXT_BACKGROUND_PADDING,
|
||||||
|
minWidth: 1,
|
||||||
|
}}
|
||||||
|
onInput={handleInput}
|
||||||
|
onBlur={onCommit}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
>
|
||||||
|
{element.content || ""}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEditor } from "@/hooks/use-editor";
|
||||||
|
import { formatTimeCode } from "@/lib/time";
|
||||||
|
import { invokeAction } from "@/lib/actions";
|
||||||
|
import { EditableTimecode } from "@/components/editable-timecode";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
FullScreenIcon,
|
||||||
|
PauseIcon,
|
||||||
|
PlayIcon,
|
||||||
|
} from "@hugeicons/core-free-icons";
|
||||||
|
import { HugeiconsIcon } from "@hugeicons/react";
|
||||||
|
import { OcSocialIcon } from "@opencut/ui/icons";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
|
||||||
|
export function PreviewToolbar({
|
||||||
|
isFullscreen,
|
||||||
|
onToggleFullscreen,
|
||||||
|
}: {
|
||||||
|
isFullscreen: boolean;
|
||||||
|
onToggleFullscreen: () => void;
|
||||||
|
}) {
|
||||||
|
const editor = useEditor();
|
||||||
|
const isPlaying = editor.playback.getIsPlaying();
|
||||||
|
const currentTime = editor.playback.getCurrentTime();
|
||||||
|
const totalDuration = editor.timeline.getTotalDuration();
|
||||||
|
const fps = editor.project.getActive().settings.fps;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-[1fr_auto_1fr] items-center pb-3 pt-5 px-5">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<EditableTimecode
|
||||||
|
time={currentTime}
|
||||||
|
duration={totalDuration}
|
||||||
|
format="HH:MM:SS:FF"
|
||||||
|
fps={fps}
|
||||||
|
onTimeChange={({ time }) => editor.playback.seek({ time })}
|
||||||
|
className="text-center"
|
||||||
|
/>
|
||||||
|
<span className="text-muted-foreground px-2 font-mono text-xs">/</span>
|
||||||
|
<span className="text-muted-foreground font-mono text-xs">
|
||||||
|
{formatTimeCode({
|
||||||
|
timeInSeconds: totalDuration,
|
||||||
|
format: "HH:MM:SS:FF",
|
||||||
|
fps,
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="text"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => invokeAction("toggle-play")}
|
||||||
|
>
|
||||||
|
<HugeiconsIcon icon={isPlaying ? PauseIcon : PlayIcon} />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<div className="justify-self-end flex items-center gap-2.5">
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
className="[&_svg]:size-auto px-1 h-7"
|
||||||
|
onClick={onToggleFullscreen}
|
||||||
|
title={isFullscreen ? "Exit fullscreen" : "Enter fullscreen"}
|
||||||
|
>
|
||||||
|
<OcSocialIcon size={20} />
|
||||||
|
</Button>
|
||||||
|
<Separator orientation="vertical" className="h-4" />
|
||||||
|
<Button
|
||||||
|
variant="text"
|
||||||
|
onClick={onToggleFullscreen}
|
||||||
|
title={isFullscreen ? "Exit fullscreen" : "Enter fullscreen"}
|
||||||
|
>
|
||||||
|
<HugeiconsIcon icon={FullScreenIcon} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,283 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useTransformHandles } from "@/hooks/use-transform-handles";
|
||||||
|
import { useEditor } from "@/hooks/use-editor";
|
||||||
|
import { isVisualElement } from "@/lib/timeline/element-utils";
|
||||||
|
import { SnapGuides } from "./snap-guides";
|
||||||
|
import { canvasToOverlay, getDisplayScale } from "@/lib/preview/preview-coords";
|
||||||
|
import type { ElementBounds } from "@/lib/preview/element-bounds";
|
||||||
|
import { cn } from "@/utils/ui";
|
||||||
|
import { HugeiconsIcon } from "@hugeicons/react";
|
||||||
|
import { Rotate01Icon } from "@hugeicons/core-free-icons";
|
||||||
|
|
||||||
|
const HANDLE_SIZE = 10;
|
||||||
|
const ROTATION_HANDLE_OFFSET = 24;
|
||||||
|
const ROTATION_HANDLE_RADIUS = 10;
|
||||||
|
const CORNER_HIT_AREA_SIZE = 18;
|
||||||
|
|
||||||
|
type Corner = "top-left" | "top-right" | "bottom-left" | "bottom-right";
|
||||||
|
const CORNERS: Corner[] = [
|
||||||
|
"top-left",
|
||||||
|
"top-right",
|
||||||
|
"bottom-left",
|
||||||
|
"bottom-right",
|
||||||
|
];
|
||||||
|
|
||||||
|
function getCornerPosition({
|
||||||
|
bounds,
|
||||||
|
corner,
|
||||||
|
}: {
|
||||||
|
bounds: ElementBounds;
|
||||||
|
corner: Corner;
|
||||||
|
}): { x: number; y: number } {
|
||||||
|
const halfW = bounds.width / 2;
|
||||||
|
const halfH = bounds.height / 2;
|
||||||
|
const angleRad = (bounds.rotation * Math.PI) / 180;
|
||||||
|
const cos = Math.cos(angleRad);
|
||||||
|
const sin = Math.sin(angleRad);
|
||||||
|
|
||||||
|
const localX =
|
||||||
|
corner === "top-left" || corner === "bottom-left" ? -halfW : halfW;
|
||||||
|
const localY =
|
||||||
|
corner === "top-left" || corner === "top-right" ? -halfH : halfH;
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: bounds.cx + (localX * cos - localY * sin),
|
||||||
|
y: bounds.cy + (localX * sin + localY * cos),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRotationHandlePosition({ bounds }: { bounds: ElementBounds }): {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
} {
|
||||||
|
const angleRad = (bounds.rotation * Math.PI) / 180;
|
||||||
|
const cos = Math.cos(angleRad);
|
||||||
|
const sin = Math.sin(angleRad);
|
||||||
|
const localY = -bounds.height / 2 - ROTATION_HANDLE_OFFSET;
|
||||||
|
return {
|
||||||
|
x: bounds.cx - localY * sin,
|
||||||
|
y: bounds.cy + localY * cos,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOverlayContext({
|
||||||
|
canvasRef,
|
||||||
|
containerRef,
|
||||||
|
}: {
|
||||||
|
canvasRef: React.RefObject<HTMLCanvasElement | null>;
|
||||||
|
containerRef: React.RefObject<HTMLDivElement | null>;
|
||||||
|
}): {
|
||||||
|
canvasRect: DOMRect;
|
||||||
|
containerRect: DOMRect;
|
||||||
|
} | null {
|
||||||
|
const canvasRect = canvasRef.current?.getBoundingClientRect();
|
||||||
|
const containerRect = containerRef.current?.getBoundingClientRect();
|
||||||
|
if (!canvasRect || !containerRect) return null;
|
||||||
|
return { canvasRect, containerRect };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TransformHandles({
|
||||||
|
canvasRef,
|
||||||
|
containerRef,
|
||||||
|
}: {
|
||||||
|
canvasRef: React.RefObject<HTMLCanvasElement | null>;
|
||||||
|
containerRef: React.RefObject<HTMLDivElement | null>;
|
||||||
|
}) {
|
||||||
|
const {
|
||||||
|
selectedWithBounds,
|
||||||
|
hasVisualSelection,
|
||||||
|
snapLines,
|
||||||
|
handleCornerPointerDown,
|
||||||
|
handleRotationPointerDown,
|
||||||
|
handlePointerMove,
|
||||||
|
handlePointerUp,
|
||||||
|
} = useTransformHandles({ canvasRef });
|
||||||
|
|
||||||
|
const editor = useEditor();
|
||||||
|
const canvasSize = editor.project.getActive().settings.canvasSize;
|
||||||
|
|
||||||
|
if (!hasVisualSelection || !selectedWithBounds) return null;
|
||||||
|
|
||||||
|
const overlayContext = getOverlayContext({ canvasRef, containerRef });
|
||||||
|
if (!overlayContext) return null;
|
||||||
|
|
||||||
|
const { bounds, element } = selectedWithBounds;
|
||||||
|
if (!isVisualElement(element)) return null;
|
||||||
|
|
||||||
|
const { canvasRect, containerRect } = overlayContext;
|
||||||
|
const displayScale = getDisplayScale({ canvasRect, canvasSize });
|
||||||
|
|
||||||
|
const toOverlay = ({
|
||||||
|
canvasX,
|
||||||
|
canvasY,
|
||||||
|
}: {
|
||||||
|
canvasX: number;
|
||||||
|
canvasY: number;
|
||||||
|
}) =>
|
||||||
|
canvasToOverlay({
|
||||||
|
canvasX,
|
||||||
|
canvasY,
|
||||||
|
canvasRect,
|
||||||
|
containerRect,
|
||||||
|
canvasSize,
|
||||||
|
});
|
||||||
|
|
||||||
|
const center = toOverlay({ canvasX: bounds.cx, canvasY: bounds.cy });
|
||||||
|
const outlineWidth = bounds.width * displayScale.x;
|
||||||
|
const outlineHeight = bounds.height * displayScale.y;
|
||||||
|
|
||||||
|
const rotationHandle = getRotationHandlePosition({ bounds });
|
||||||
|
const rotationHandleScreen = toOverlay({
|
||||||
|
canvasX: rotationHandle.x,
|
||||||
|
canvasY: rotationHandle.y,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="pointer-events-none absolute inset-0" aria-hidden>
|
||||||
|
<SnapGuides
|
||||||
|
lines={snapLines}
|
||||||
|
canvasRef={canvasRef}
|
||||||
|
containerRef={containerRef}
|
||||||
|
/>
|
||||||
|
<BoundingBoxOutline
|
||||||
|
center={center}
|
||||||
|
outlineWidth={outlineWidth}
|
||||||
|
outlineHeight={outlineHeight}
|
||||||
|
rotation={bounds.rotation}
|
||||||
|
/>
|
||||||
|
{CORNERS.map((corner) => {
|
||||||
|
const cornerPosition = getCornerPosition({ bounds, corner });
|
||||||
|
const screen = toOverlay({
|
||||||
|
canvasX: cornerPosition.x,
|
||||||
|
canvasY: cornerPosition.y,
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<CornerHandle
|
||||||
|
key={corner}
|
||||||
|
corner={corner}
|
||||||
|
screen={screen}
|
||||||
|
onPointerDown={(event) =>
|
||||||
|
handleCornerPointerDown({ event, corner })
|
||||||
|
}
|
||||||
|
onPointerMove={(event) => handlePointerMove({ event })}
|
||||||
|
onPointerUp={(event) => handlePointerUp({ event })}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<RotationHandle
|
||||||
|
screen={rotationHandleScreen}
|
||||||
|
onPointerDown={(event) => handleRotationPointerDown({ event })}
|
||||||
|
onPointerMove={(event) => handlePointerMove({ event })}
|
||||||
|
onPointerUp={(event) => handlePointerUp({ event })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function BoundingBoxOutline({
|
||||||
|
center,
|
||||||
|
outlineWidth,
|
||||||
|
outlineHeight,
|
||||||
|
rotation,
|
||||||
|
}: {
|
||||||
|
center: { x: number; y: number };
|
||||||
|
outlineWidth: number;
|
||||||
|
outlineHeight: number;
|
||||||
|
rotation: number;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="pointer-events-none absolute"
|
||||||
|
style={{
|
||||||
|
left: center.x - outlineWidth / 2,
|
||||||
|
top: center.y - outlineHeight / 2,
|
||||||
|
width: outlineWidth,
|
||||||
|
height: outlineHeight,
|
||||||
|
transform: `rotate(${rotation}deg)`,
|
||||||
|
transformOrigin: "center center",
|
||||||
|
border: "1px solid white",
|
||||||
|
boxSizing: "border-box",
|
||||||
|
opacity: 0.75,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CornerHandle({
|
||||||
|
corner,
|
||||||
|
screen,
|
||||||
|
onPointerDown,
|
||||||
|
onPointerMove,
|
||||||
|
onPointerUp,
|
||||||
|
}: {
|
||||||
|
corner: Corner;
|
||||||
|
screen: { x: number; y: number };
|
||||||
|
onPointerDown: (event: React.PointerEvent) => void;
|
||||||
|
onPointerMove: (event: React.PointerEvent) => void;
|
||||||
|
onPointerUp: (event: React.PointerEvent) => void;
|
||||||
|
}) {
|
||||||
|
const isNesw = corner === "top-right" || corner === "bottom-left";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={cn(
|
||||||
|
"absolute flex items-center justify-center outline-none",
|
||||||
|
isNesw ? "cursor-nesw-resize" : "cursor-nwse-resize",
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
left: screen.x - CORNER_HIT_AREA_SIZE / 2,
|
||||||
|
top: screen.y - CORNER_HIT_AREA_SIZE / 2,
|
||||||
|
width: CORNER_HIT_AREA_SIZE,
|
||||||
|
height: CORNER_HIT_AREA_SIZE,
|
||||||
|
pointerEvents: "auto",
|
||||||
|
}}
|
||||||
|
onPointerDown={onPointerDown}
|
||||||
|
onPointerMove={onPointerMove}
|
||||||
|
onPointerUp={onPointerUp}
|
||||||
|
onPointerLeave={onPointerUp}
|
||||||
|
onKeyDown={(event) => event.key === "Enter" && event.preventDefault()}
|
||||||
|
onKeyUp={(event) => event.key === "Enter" && event.preventDefault()}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="rounded-sm bg-white"
|
||||||
|
style={{ width: HANDLE_SIZE, height: HANDLE_SIZE }}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function RotationHandle({
|
||||||
|
screen,
|
||||||
|
onPointerDown,
|
||||||
|
onPointerMove,
|
||||||
|
onPointerUp,
|
||||||
|
}: {
|
||||||
|
screen: { x: number; y: number };
|
||||||
|
onPointerDown: (event: React.PointerEvent) => void;
|
||||||
|
onPointerMove: (event: React.PointerEvent) => void;
|
||||||
|
onPointerUp: (event: React.PointerEvent) => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="absolute flex items-center justify-center rounded-full bg-white text-black shadow-sm outline-none"
|
||||||
|
style={{
|
||||||
|
left: screen.x - ROTATION_HANDLE_RADIUS,
|
||||||
|
top: screen.y - ROTATION_HANDLE_RADIUS,
|
||||||
|
width: ROTATION_HANDLE_RADIUS * 2,
|
||||||
|
height: ROTATION_HANDLE_RADIUS * 2,
|
||||||
|
pointerEvents: "auto",
|
||||||
|
}}
|
||||||
|
onPointerDown={onPointerDown}
|
||||||
|
onPointerMove={onPointerMove}
|
||||||
|
onPointerUp={onPointerUp}
|
||||||
|
onPointerLeave={onPointerUp}
|
||||||
|
onKeyDown={(event) => event.key === "Enter" && event.preventDefault()}
|
||||||
|
onKeyUp={(event) => event.key === "Enter" && event.preventDefault()}
|
||||||
|
>
|
||||||
|
<HugeiconsIcon icon={Rotate01Icon} className="size-3" strokeWidth={2.5} />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { HugeiconsIcon } from "@hugeicons/react";
|
||||||
|
import { Settings05Icon } from "@hugeicons/core-free-icons";
|
||||||
|
|
||||||
|
export function EmptyView() {
|
||||||
|
return (
|
||||||
|
<div className="bg-background flex h-full flex-col items-center justify-center gap-3 p-4">
|
||||||
|
<HugeiconsIcon
|
||||||
|
icon={Settings05Icon}
|
||||||
|
className="text-muted-foreground/75 size-10"
|
||||||
|
strokeWidth={1}
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col gap-2 text-center">
|
||||||
|
<p className="text-lg font-medium ">It's empty here</p>
|
||||||
|
<p className="text-muted-foreground text-sm text-balance">
|
||||||
|
Click an element on the timeline to edit its properties
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import { useReducer, useRef } from "react";
|
||||||
|
import { evaluateMathExpression } from "@/utils/math";
|
||||||
|
|
||||||
|
function looksLikeExpression({ input }: { input: string }): boolean {
|
||||||
|
const trimmed = input.trim();
|
||||||
|
if (!trimmed) return false;
|
||||||
|
if (/[+*/]/.test(input)) return true;
|
||||||
|
const minusIndex = trimmed.indexOf("-");
|
||||||
|
return minusIndex > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePropertyDraft<T>({
|
||||||
|
displayValue: sourceDisplay,
|
||||||
|
parse,
|
||||||
|
onPreview,
|
||||||
|
onCommit,
|
||||||
|
supportsExpressions = true,
|
||||||
|
}: {
|
||||||
|
displayValue: string;
|
||||||
|
parse: (input: string) => T | null;
|
||||||
|
onPreview: (value: T) => void;
|
||||||
|
onCommit: () => void;
|
||||||
|
supportsExpressions?: boolean;
|
||||||
|
}) {
|
||||||
|
const [, forceRender] = useReducer(
|
||||||
|
(renderVersion: number) => renderVersion + 1,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
const isEditing = useRef(false);
|
||||||
|
const draft = useRef("");
|
||||||
|
|
||||||
|
return {
|
||||||
|
displayValue: isEditing.current ? draft.current : sourceDisplay,
|
||||||
|
scrubTo: (value: number) => {
|
||||||
|
const parsed = parse(String(value));
|
||||||
|
if (parsed !== null) onPreview(parsed);
|
||||||
|
},
|
||||||
|
commitScrub: onCommit,
|
||||||
|
onFocus: () => {
|
||||||
|
isEditing.current = true;
|
||||||
|
draft.current = sourceDisplay;
|
||||||
|
forceRender();
|
||||||
|
},
|
||||||
|
onChange: (
|
||||||
|
event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||||
|
) => {
|
||||||
|
draft.current = event.target.value;
|
||||||
|
forceRender();
|
||||||
|
|
||||||
|
const parsed = parse(event.target.value);
|
||||||
|
if (parsed !== null) {
|
||||||
|
onPreview(parsed);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onBlur: () => {
|
||||||
|
if (
|
||||||
|
supportsExpressions &&
|
||||||
|
looksLikeExpression({ input: draft.current })
|
||||||
|
) {
|
||||||
|
const evaluated = evaluateMathExpression({ input: draft.current });
|
||||||
|
if (evaluated !== null) {
|
||||||
|
const parsed = parse(String(evaluated));
|
||||||
|
if (parsed !== null) onPreview(parsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onCommit();
|
||||||
|
isEditing.current = false;
|
||||||
|
draft.current = "";
|
||||||
|
forceRender();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -4,8 +4,7 @@ import { ScrollArea } from "@/components/ui/scroll-area";
|
|||||||
import { AudioProperties } from "./audio-properties";
|
import { AudioProperties } from "./audio-properties";
|
||||||
import { VideoProperties } from "./video-properties";
|
import { VideoProperties } from "./video-properties";
|
||||||
import { TextProperties } from "./text-properties";
|
import { TextProperties } from "./text-properties";
|
||||||
import { HugeiconsIcon } from "@hugeicons/react";
|
import { EmptyView } from "./empty-view";
|
||||||
import { Settings05Icon } from "@hugeicons/core-free-icons";
|
|
||||||
import { useEditor } from "@/hooks/use-editor";
|
import { useEditor } from "@/hooks/use-editor";
|
||||||
import { useElementSelection } from "@/hooks/timeline/element/use-element-selection";
|
import { useElementSelection } from "@/hooks/timeline/element/use-element-selection";
|
||||||
|
|
||||||
@@ -18,9 +17,9 @@ export function PropertiesPanel() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="panel bg-background h-full rounded-sm border overflow-hidden">
|
||||||
{selectedElements.length > 0 ? (
|
{selectedElements.length > 0 ? (
|
||||||
<ScrollArea className="bg-panel h-full rounded-sm">
|
<ScrollArea className="h-full">
|
||||||
{elementsWithTracks.map(({ track, element }) => {
|
{elementsWithTracks.map(({ track, element }) => {
|
||||||
if (element.type === "text") {
|
if (element.type === "text") {
|
||||||
return (
|
return (
|
||||||
@@ -45,24 +44,6 @@ export function PropertiesPanel() {
|
|||||||
) : (
|
) : (
|
||||||
<EmptyView />
|
<EmptyView />
|
||||||
)}
|
)}
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function EmptyView() {
|
|
||||||
return (
|
|
||||||
<div className="bg-panel flex h-full flex-col items-center justify-center gap-3 p-4">
|
|
||||||
<HugeiconsIcon
|
|
||||||
icon={Settings05Icon}
|
|
||||||
className="text-muted-foreground/75 size-10"
|
|
||||||
strokeWidth={1}
|
|
||||||
/>
|
|
||||||
<div className="flex flex-col gap-2 text-center">
|
|
||||||
<p className="text-lg font-medium">It's empty here</p>
|
|
||||||
<p className="text-muted-foreground text-sm text-balance">
|
|
||||||
Click an element on the timeline to edit its properties
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,91 +0,0 @@
|
|||||||
import { useState } from "react";
|
|
||||||
import { cn } from "@/utils/ui";
|
|
||||||
import { HugeiconsIcon } from "@hugeicons/react";
|
|
||||||
import { ArrowDownIcon } from "@hugeicons/core-free-icons";
|
|
||||||
|
|
||||||
interface PropertyItemProps {
|
|
||||||
direction?: "row" | "column";
|
|
||||||
children: React.ReactNode;
|
|
||||||
className?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function PropertyItem({
|
|
||||||
direction = "row",
|
|
||||||
children,
|
|
||||||
className,
|
|
||||||
}: PropertyItemProps) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"flex gap-2",
|
|
||||||
direction === "row"
|
|
||||||
? "items-center justify-between gap-6"
|
|
||||||
: "flex-col gap-1.5",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function PropertyItemLabel({
|
|
||||||
children,
|
|
||||||
className,
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode;
|
|
||||||
className?: string;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<span className={cn("text-muted-foreground text-xs", className)}>
|
|
||||||
{children}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function PropertyItemValue({
|
|
||||||
children,
|
|
||||||
className,
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode;
|
|
||||||
className?: string;
|
|
||||||
}) {
|
|
||||||
return <div className={cn("flex-1 text-sm", className)}>{children}</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PropertyGroupProps {
|
|
||||||
title: string;
|
|
||||||
children: React.ReactNode;
|
|
||||||
defaultExpanded?: boolean;
|
|
||||||
className?: string;
|
|
||||||
titleClassName?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function PropertyGroup({
|
|
||||||
title,
|
|
||||||
children,
|
|
||||||
defaultExpanded = true,
|
|
||||||
className,
|
|
||||||
titleClassName,
|
|
||||||
}: PropertyGroupProps) {
|
|
||||||
const [isExpanded, setIsExpanded] = useState(defaultExpanded);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PropertyItem direction="column" className={cn("gap-3", className)}>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="flex items-center gap-1.5 cursor-pointer"
|
|
||||||
onClick={() => setIsExpanded(!isExpanded)}
|
|
||||||
>
|
|
||||||
<PropertyItemLabel className={cn(titleClassName)}>
|
|
||||||
{title}
|
|
||||||
</PropertyItemLabel>
|
|
||||||
<HugeiconsIcon
|
|
||||||
icon={ArrowDownIcon}
|
|
||||||
className={cn("size-3", !isExpanded && "-rotate-90")}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
{isExpanded && <PropertyItemValue>{children}</PropertyItemValue>}
|
|
||||||
</PropertyItem>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
import { createContext, useContext, useState } from "react";
|
||||||
|
import { cn } from "@/utils/ui";
|
||||||
|
import { HugeiconsIcon } from "@hugeicons/react";
|
||||||
|
import { ArrowDownIcon } from "@hugeicons/core-free-icons";
|
||||||
|
|
||||||
|
const sectionExpandedCache = new Map<string, boolean>();
|
||||||
|
|
||||||
|
interface SectionContext {
|
||||||
|
isOpen: boolean;
|
||||||
|
toggle: () => void;
|
||||||
|
collapsible: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SectionCtx = createContext<SectionContext | null>(null);
|
||||||
|
|
||||||
|
function useSectionContext() {
|
||||||
|
return useContext(SectionCtx);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SectionProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
collapsible?: boolean;
|
||||||
|
defaultOpen?: boolean;
|
||||||
|
sectionKey?: string;
|
||||||
|
className?: string;
|
||||||
|
hasBorderTop?: boolean;
|
||||||
|
hasBorderBottom?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Section({
|
||||||
|
children,
|
||||||
|
collapsible = false,
|
||||||
|
defaultOpen = true,
|
||||||
|
sectionKey,
|
||||||
|
className,
|
||||||
|
hasBorderTop = true,
|
||||||
|
hasBorderBottom = true,
|
||||||
|
}: SectionProps) {
|
||||||
|
const cached = sectionKey ? sectionExpandedCache.get(sectionKey) : undefined;
|
||||||
|
const [isOpen, setIsOpen] = useState(cached ?? defaultOpen);
|
||||||
|
|
||||||
|
const toggle = () => {
|
||||||
|
const next = !isOpen;
|
||||||
|
setIsOpen(next);
|
||||||
|
if (sectionKey) sectionExpandedCache.set(sectionKey, next);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SectionCtx.Provider value={{ isOpen, toggle, collapsible }}>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col",
|
||||||
|
hasBorderTop && "border-t",
|
||||||
|
hasBorderBottom && "last:border-b",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</SectionCtx.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SectionHeaderProps {
|
||||||
|
title: string;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
onClick?: () => void;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SectionHeader({
|
||||||
|
title,
|
||||||
|
children,
|
||||||
|
onClick,
|
||||||
|
className,
|
||||||
|
}: SectionHeaderProps) {
|
||||||
|
const ctx = useSectionContext();
|
||||||
|
const isCollapsible = ctx?.collapsible ?? false;
|
||||||
|
const isOpen = ctx?.isOpen ?? true;
|
||||||
|
const isInteractive = isCollapsible || !!onClick;
|
||||||
|
|
||||||
|
const handleClick = isCollapsible ? ctx?.toggle : onClick;
|
||||||
|
|
||||||
|
const content = (
|
||||||
|
<>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"text-sm font-medium",
|
||||||
|
isOpen ? "text-foreground" : "text-muted-foreground",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</span>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
{children}
|
||||||
|
{isCollapsible && (
|
||||||
|
<HugeiconsIcon
|
||||||
|
icon={ArrowDownIcon}
|
||||||
|
className={cn(
|
||||||
|
"size-3 shrink-0 transition-transform duration-200 ease-out",
|
||||||
|
isOpen
|
||||||
|
? "rotate-0 text-foreground"
|
||||||
|
: "-rotate-90 text-muted-foreground",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const baseClassName = cn(
|
||||||
|
"flex w-full items-center justify-between h-11 px-3.5",
|
||||||
|
className,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isInteractive) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={cn(baseClassName, "cursor-pointer text-left")}
|
||||||
|
onClick={(event) => {
|
||||||
|
handleClick?.();
|
||||||
|
event.currentTarget.blur();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className={baseClassName}>{content}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SectionContent({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
const ctx = useSectionContext();
|
||||||
|
const isCollapsible = ctx?.collapsible ?? false;
|
||||||
|
const isOpen = ctx?.isOpen ?? true;
|
||||||
|
|
||||||
|
if (isCollapsible) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"grid transition-[grid-template-rows] duration-100 ease-out",
|
||||||
|
isOpen ? "grid-rows-[1fr]" : "grid-rows-[0fr]",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="overflow-hidden">
|
||||||
|
<div className={cn("p-4 pt-0", className)}>{children}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className={cn("p-4 pt-0", className)}>{children}</div>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
import { useEditor } from "@/hooks/use-editor";
|
||||||
|
import { usePropertyDraft } from "../hooks/use-property-draft";
|
||||||
|
import { clamp } from "@/utils/math";
|
||||||
|
import { NumberField } from "@/components/ui/number-field";
|
||||||
|
import {
|
||||||
|
DEFAULT_BLEND_MODE,
|
||||||
|
DEFAULT_OPACITY,
|
||||||
|
} from "@/constants/timeline-constants";
|
||||||
|
import { OcCheckerboardIcon } from "@opencut/ui/icons";
|
||||||
|
import { Fragment, useRef } from "react";
|
||||||
|
import { Section, SectionContent, SectionHeader } from "../section";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectSeparator,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import type { BlendMode } from "@/types/rendering";
|
||||||
|
import type { ElementType } from "@/types/timeline";
|
||||||
|
import { HugeiconsIcon } from "@hugeicons/react";
|
||||||
|
import { RainDropIcon } from "@hugeicons/core-free-icons";
|
||||||
|
|
||||||
|
type BlendingElement = {
|
||||||
|
id: string;
|
||||||
|
opacity: number;
|
||||||
|
type: ElementType;
|
||||||
|
blendMode?: BlendMode;
|
||||||
|
};
|
||||||
|
|
||||||
|
const BLEND_MODE_GROUPS = [
|
||||||
|
[{ value: "normal", label: "Normal" }],
|
||||||
|
[
|
||||||
|
{ value: "darken", label: "Darken" },
|
||||||
|
{ value: "multiply", label: "Multiply" },
|
||||||
|
{ value: "color-burn", label: "Color Burn" },
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{ value: "lighten", label: "Lighten" },
|
||||||
|
{ value: "screen", label: "Screen" },
|
||||||
|
{ value: "plus-lighter", label: "Plus Lighter" },
|
||||||
|
{ value: "color-dodge", label: "Color Dodge" },
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{ value: "overlay", label: "Overlay" },
|
||||||
|
{ value: "soft-light", label: "Soft Light" },
|
||||||
|
{ value: "hard-light", label: "Hard Light" },
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{ value: "difference", label: "Difference" },
|
||||||
|
{ value: "exclusion", label: "Exclusion" },
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{ value: "hue", label: "Hue" },
|
||||||
|
{ value: "saturation", label: "Saturation" },
|
||||||
|
{ value: "color", label: "Color" },
|
||||||
|
{ value: "luminosity", label: "Luminosity" },
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
export function BlendingSection({
|
||||||
|
element,
|
||||||
|
trackId,
|
||||||
|
}: {
|
||||||
|
element: BlendingElement;
|
||||||
|
trackId: string;
|
||||||
|
}) {
|
||||||
|
const editor = useEditor();
|
||||||
|
const blendMode = element.blendMode ?? DEFAULT_BLEND_MODE;
|
||||||
|
const didSelectRef = useRef(false);
|
||||||
|
const committedBlendModeRef = useRef(blendMode);
|
||||||
|
if (!editor.timeline.isPreviewActive()) {
|
||||||
|
committedBlendModeRef.current = blendMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const previewBlendMode = (value: BlendMode) =>
|
||||||
|
editor.timeline.previewElements({
|
||||||
|
updates: [
|
||||||
|
{ trackId, elementId: element.id, updates: { blendMode: value } },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const commitBlendMode = (value: string) => {
|
||||||
|
if (editor.timeline.isPreviewActive()) {
|
||||||
|
editor.timeline.commitPreview();
|
||||||
|
} else {
|
||||||
|
editor.timeline.updateElements({
|
||||||
|
updates: [
|
||||||
|
{
|
||||||
|
trackId,
|
||||||
|
elementId: element.id,
|
||||||
|
updates: { blendMode: value as BlendMode },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
didSelectRef.current = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBlendModeOpenChange = (isOpen: boolean) => {
|
||||||
|
if (!isOpen) {
|
||||||
|
if (!didSelectRef.current) editor.timeline.discardPreview();
|
||||||
|
didSelectRef.current = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const opacity = usePropertyDraft({
|
||||||
|
displayValue: Math.round(element.opacity * 100).toString(),
|
||||||
|
parse: (input) => {
|
||||||
|
const parsed = parseFloat(input);
|
||||||
|
if (Number.isNaN(parsed)) return null;
|
||||||
|
return clamp({ value: parsed, min: 0, max: 100 }) / 100;
|
||||||
|
},
|
||||||
|
onPreview: (value) =>
|
||||||
|
editor.timeline.previewElements({
|
||||||
|
updates: [
|
||||||
|
{ trackId, elementId: element.id, updates: { opacity: value } },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
onCommit: () => editor.timeline.commitPreview(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Section collapsible sectionKey={`${element.type}:blending`}>
|
||||||
|
<SectionHeader title="Blending" />
|
||||||
|
<SectionContent>
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<div className="w-1/2 space-y-1.5">
|
||||||
|
<NumberField
|
||||||
|
className="w-full"
|
||||||
|
icon={
|
||||||
|
<OcCheckerboardIcon className="size-3.5 text-muted-foreground" />
|
||||||
|
}
|
||||||
|
value={opacity.displayValue}
|
||||||
|
min={0}
|
||||||
|
max={100}
|
||||||
|
onFocus={opacity.onFocus}
|
||||||
|
onChange={opacity.onChange}
|
||||||
|
onBlur={opacity.onBlur}
|
||||||
|
onScrub={opacity.scrubTo}
|
||||||
|
onScrubEnd={opacity.commitScrub}
|
||||||
|
onReset={() =>
|
||||||
|
editor.timeline.updateElements({
|
||||||
|
updates: [
|
||||||
|
{
|
||||||
|
trackId,
|
||||||
|
elementId: element.id,
|
||||||
|
updates: { opacity: DEFAULT_OPACITY },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
isDefault={element.opacity === DEFAULT_OPACITY}
|
||||||
|
dragSensitivity="slow"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="w-1/2 space-y-1.5">
|
||||||
|
<Select
|
||||||
|
value={committedBlendModeRef.current}
|
||||||
|
onOpenChange={handleBlendModeOpenChange}
|
||||||
|
onValueChange={commitBlendMode}
|
||||||
|
>
|
||||||
|
<SelectTrigger
|
||||||
|
icon={<HugeiconsIcon icon={RainDropIcon} />}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<SelectValue placeholder="Select blend mode" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent className="w-36">
|
||||||
|
{BLEND_MODE_GROUPS.map((group, groupIndex) => (
|
||||||
|
<Fragment key={group[0]?.value ?? `group-${groupIndex}`}>
|
||||||
|
{group.map((option) => (
|
||||||
|
<SelectItem
|
||||||
|
key={option.value}
|
||||||
|
value={option.value}
|
||||||
|
onPointerEnter={() =>
|
||||||
|
previewBlendMode(option.value as BlendMode)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
{groupIndex < BLEND_MODE_GROUPS.length - 1 ? (
|
||||||
|
<SelectSeparator />
|
||||||
|
) : null}
|
||||||
|
</Fragment>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SectionContent>
|
||||||
|
</Section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from "./transform";
|
||||||
|
export * from "./blending";
|
||||||
@@ -0,0 +1,248 @@
|
|||||||
|
import { NumberField } from "@/components/ui/number-field";
|
||||||
|
import { useEditor } from "@/hooks/use-editor";
|
||||||
|
import { usePropertyDraft } from "../hooks/use-property-draft";
|
||||||
|
import { clamp } from "@/utils/math";
|
||||||
|
import type { ElementType, Transform } from "@/types/timeline";
|
||||||
|
import { Section, SectionContent, SectionHeader } from "../section";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { HugeiconsIcon } from "@hugeicons/react";
|
||||||
|
import {
|
||||||
|
ArrowExpandIcon,
|
||||||
|
Link05Icon,
|
||||||
|
RotateClockwiseIcon,
|
||||||
|
} from "@hugeicons/core-free-icons";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { DEFAULT_TRANSFORM } from "@/constants/timeline-constants";
|
||||||
|
|
||||||
|
type TransformElement = {
|
||||||
|
id: string;
|
||||||
|
transform: Transform;
|
||||||
|
type: ElementType;
|
||||||
|
};
|
||||||
|
|
||||||
|
function parseFloat_({ input }: { input: string }): number | null {
|
||||||
|
const parsed = parseFloat(input);
|
||||||
|
return Number.isNaN(parsed) ? null : parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TransformSection({
|
||||||
|
element,
|
||||||
|
trackId,
|
||||||
|
}: {
|
||||||
|
element: TransformElement;
|
||||||
|
trackId: string;
|
||||||
|
}) {
|
||||||
|
const editor = useEditor();
|
||||||
|
const [isScaleLocked, setIsScaleLocked] = useState(false);
|
||||||
|
|
||||||
|
const previewTransform = (transform: Partial<Transform>) => {
|
||||||
|
editor.timeline.previewElements({
|
||||||
|
updates: [
|
||||||
|
{
|
||||||
|
trackId,
|
||||||
|
elementId: element.id,
|
||||||
|
updates: { transform: { ...element.transform, ...transform } },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const commit = () => editor.timeline.commitPreview();
|
||||||
|
|
||||||
|
const positionX = usePropertyDraft({
|
||||||
|
displayValue: Math.round(element.transform.position.x).toString(),
|
||||||
|
parse: (input) => parseFloat_({ input }),
|
||||||
|
onPreview: (value) =>
|
||||||
|
previewTransform({
|
||||||
|
position: { ...element.transform.position, x: value },
|
||||||
|
}),
|
||||||
|
onCommit: commit,
|
||||||
|
});
|
||||||
|
|
||||||
|
const positionY = usePropertyDraft({
|
||||||
|
displayValue: Math.round(element.transform.position.y).toString(),
|
||||||
|
parse: (input) => parseFloat_({ input }),
|
||||||
|
onPreview: (value) =>
|
||||||
|
previewTransform({
|
||||||
|
position: { ...element.transform.position, y: value },
|
||||||
|
}),
|
||||||
|
onCommit: commit,
|
||||||
|
});
|
||||||
|
|
||||||
|
const scale = usePropertyDraft({
|
||||||
|
displayValue: Math.round(element.transform.scale * 100).toString(),
|
||||||
|
parse: (input) => {
|
||||||
|
const parsed = parseFloat_({ input });
|
||||||
|
if (parsed === null) return null;
|
||||||
|
return Math.max(parsed, 1) / 100;
|
||||||
|
},
|
||||||
|
onPreview: (value) => previewTransform({ scale: value }),
|
||||||
|
onCommit: commit,
|
||||||
|
});
|
||||||
|
const scaleFieldProps = {
|
||||||
|
className: "flex-1",
|
||||||
|
value: scale.displayValue,
|
||||||
|
onFocus: scale.onFocus,
|
||||||
|
onChange: scale.onChange,
|
||||||
|
onBlur: scale.onBlur,
|
||||||
|
dragSensitivity: "slow" as const,
|
||||||
|
onScrub: scale.scrubTo,
|
||||||
|
onScrubEnd: scale.commitScrub,
|
||||||
|
onReset: () =>
|
||||||
|
editor.timeline.updateElements({
|
||||||
|
updates: [
|
||||||
|
{
|
||||||
|
trackId,
|
||||||
|
elementId: element.id,
|
||||||
|
updates: {
|
||||||
|
transform: {
|
||||||
|
...element.transform,
|
||||||
|
scale: DEFAULT_TRANSFORM.scale,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
isDefault: element.transform.scale === DEFAULT_TRANSFORM.scale,
|
||||||
|
};
|
||||||
|
|
||||||
|
const rotation = usePropertyDraft({
|
||||||
|
displayValue: Math.round(element.transform.rotate).toString(),
|
||||||
|
parse: (input) => {
|
||||||
|
const parsed = parseFloat_({ input });
|
||||||
|
if (parsed === null) return null;
|
||||||
|
return clamp({ value: parsed, min: -360, max: 360 });
|
||||||
|
},
|
||||||
|
onPreview: (value) => previewTransform({ rotate: value }),
|
||||||
|
onCommit: commit,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Section collapsible sectionKey={`${element.type}:transform`}>
|
||||||
|
<SectionHeader title="Transform" />
|
||||||
|
<SectionContent>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{isScaleLocked ? (
|
||||||
|
<>
|
||||||
|
<NumberField icon="W" {...scaleFieldProps} />
|
||||||
|
<NumberField icon="H" {...scaleFieldProps} />
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<NumberField
|
||||||
|
icon={<HugeiconsIcon icon={ArrowExpandIcon} />}
|
||||||
|
{...scaleFieldProps}
|
||||||
|
className="flex-1"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
variant={isScaleLocked ? "secondary" : "ghost"}
|
||||||
|
size="icon"
|
||||||
|
aria-pressed={isScaleLocked}
|
||||||
|
onClick={() => setIsScaleLocked((isLocked) => !isLocked)}
|
||||||
|
>
|
||||||
|
<HugeiconsIcon icon={Link05Icon} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<NumberField
|
||||||
|
icon="X"
|
||||||
|
className="flex-1"
|
||||||
|
value={positionX.displayValue}
|
||||||
|
onFocus={positionX.onFocus}
|
||||||
|
onChange={positionX.onChange}
|
||||||
|
onBlur={positionX.onBlur}
|
||||||
|
onScrub={positionX.scrubTo}
|
||||||
|
onScrubEnd={positionX.commitScrub}
|
||||||
|
onReset={() =>
|
||||||
|
editor.timeline.updateElements({
|
||||||
|
updates: [
|
||||||
|
{
|
||||||
|
trackId,
|
||||||
|
elementId: element.id,
|
||||||
|
updates: {
|
||||||
|
transform: {
|
||||||
|
...element.transform,
|
||||||
|
position: {
|
||||||
|
...element.transform.position,
|
||||||
|
x: DEFAULT_TRANSFORM.position.x,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
isDefault={
|
||||||
|
element.transform.position.x === DEFAULT_TRANSFORM.position.x
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<NumberField
|
||||||
|
icon="Y"
|
||||||
|
className="flex-1"
|
||||||
|
value={positionY.displayValue}
|
||||||
|
onFocus={positionY.onFocus}
|
||||||
|
onChange={positionY.onChange}
|
||||||
|
onBlur={positionY.onBlur}
|
||||||
|
onScrub={positionY.scrubTo}
|
||||||
|
onScrubEnd={positionY.commitScrub}
|
||||||
|
onReset={() =>
|
||||||
|
editor.timeline.updateElements({
|
||||||
|
updates: [
|
||||||
|
{
|
||||||
|
trackId,
|
||||||
|
elementId: element.id,
|
||||||
|
updates: {
|
||||||
|
transform: {
|
||||||
|
...element.transform,
|
||||||
|
position: {
|
||||||
|
...element.transform.position,
|
||||||
|
y: DEFAULT_TRANSFORM.position.y,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
isDefault={
|
||||||
|
element.transform.position.y === DEFAULT_TRANSFORM.position.y
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<NumberField
|
||||||
|
icon={<HugeiconsIcon icon={RotateClockwiseIcon} />}
|
||||||
|
className="flex-1"
|
||||||
|
value={rotation.displayValue}
|
||||||
|
onFocus={rotation.onFocus}
|
||||||
|
onChange={rotation.onChange}
|
||||||
|
onBlur={rotation.onBlur}
|
||||||
|
dragSensitivity="slow"
|
||||||
|
onScrub={rotation.scrubTo}
|
||||||
|
onScrubEnd={rotation.commitScrub}
|
||||||
|
onReset={() =>
|
||||||
|
editor.timeline.updateElements({
|
||||||
|
updates: [
|
||||||
|
{
|
||||||
|
trackId,
|
||||||
|
elementId: element.id,
|
||||||
|
updates: {
|
||||||
|
transform: {
|
||||||
|
...element.transform,
|
||||||
|
rotate: DEFAULT_TRANSFORM.rotate,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
isDefault={element.transform.rotate === DEFAULT_TRANSFORM.rotate}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SectionContent>
|
||||||
|
</Section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,35 +1,31 @@
|
|||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { FontPicker } from "@/components/ui/font-picker";
|
import { FontPicker } from "@/components/ui/font-picker";
|
||||||
import type { FontFamily } from "@/constants/font-constants";
|
|
||||||
import type { TextElement } from "@/types/timeline";
|
import type { TextElement } from "@/types/timeline";
|
||||||
import { Slider } from "@/components/ui/slider";
|
import { Slider } from "@/components/ui/slider";
|
||||||
import { Input } from "@/components/ui/input";
|
import { NumberField } from "@/components/ui/number-field";
|
||||||
import { Button } from "@/components/ui/button";
|
import { useRef } from "react";
|
||||||
import { useState, useRef } from "react";
|
import { Section, SectionContent, SectionHeader } from "./section";
|
||||||
import { PanelBaseView } from "@/components/editor/panels/panel-base-view";
|
|
||||||
import {
|
|
||||||
TEXT_PROPERTIES_TABS,
|
|
||||||
isTextPropertiesTab,
|
|
||||||
useTextPropertiesStore,
|
|
||||||
} from "@/stores/text-properties-store";
|
|
||||||
import {
|
|
||||||
PropertyItem,
|
|
||||||
PropertyItemLabel,
|
|
||||||
PropertyItemValue,
|
|
||||||
} from "./property-item";
|
|
||||||
import { ColorPicker } from "@/components/ui/color-picker";
|
import { ColorPicker } from "@/components/ui/color-picker";
|
||||||
import { cn } from "@/utils/ui";
|
import { uppercase } from "@/utils/string";
|
||||||
import { capitalizeFirstLetter, uppercase } from "@/utils/string";
|
|
||||||
import { clamp } from "@/utils/math";
|
import { clamp } from "@/utils/math";
|
||||||
import { HugeiconsIcon } from "@hugeicons/react";
|
|
||||||
import { LayoutGridIcon } from "@hugeicons/core-free-icons";
|
|
||||||
import {
|
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
TooltipTrigger,
|
|
||||||
} from "@/components/ui/tooltip";
|
|
||||||
import { useEditor } from "@/hooks/use-editor";
|
import { useEditor } from "@/hooks/use-editor";
|
||||||
import { DEFAULT_COLOR } from "@/constants/project-constants";
|
import { DEFAULT_COLOR } from "@/constants/project-constants";
|
||||||
|
import {
|
||||||
|
DEFAULT_TEXT_ELEMENT,
|
||||||
|
MAX_FONT_SIZE,
|
||||||
|
MIN_FONT_SIZE,
|
||||||
|
} from "@/constants/text-constants";
|
||||||
|
import { usePropertyDraft } from "./hooks/use-property-draft";
|
||||||
|
import { TransformSection, BlendingSection } from "./sections";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { OcFontWeightIcon } from "@opencut/ui/icons";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
|
||||||
export function TextProperties({
|
export function TextProperties({
|
||||||
element,
|
element,
|
||||||
@@ -38,373 +34,229 @@ export function TextProperties({
|
|||||||
element: TextElement;
|
element: TextElement;
|
||||||
trackId: string;
|
trackId: string;
|
||||||
}) {
|
}) {
|
||||||
const editor = useEditor();
|
|
||||||
const { activeTab, setActiveTab } = useTextPropertiesStore();
|
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
|
||||||
const [fontSizeInput, setFontSizeInput] = useState(
|
|
||||||
element.fontSize.toString(),
|
|
||||||
);
|
|
||||||
const [opacityInput, setOpacityInput] = useState(
|
|
||||||
Math.round(element.opacity * 100).toString(),
|
|
||||||
);
|
|
||||||
|
|
||||||
const lastSelectedColor = useRef(DEFAULT_COLOR);
|
|
||||||
|
|
||||||
const handleFontSizeChange = ({ value }: { value: string }) => {
|
|
||||||
setFontSizeInput(value);
|
|
||||||
|
|
||||||
if (value.trim() !== "") {
|
|
||||||
const parsed = parseInt(value, 10);
|
|
||||||
const fontSize = Number.isNaN(parsed)
|
|
||||||
? element.fontSize
|
|
||||||
: clamp({ value: parsed, min: 8, max: 300 });
|
|
||||||
editor.timeline.updateTextElement({
|
|
||||||
trackId,
|
|
||||||
elementId: element.id,
|
|
||||||
updates: { fontSize },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFontSizeBlur = () => {
|
|
||||||
const parsed = parseInt(fontSizeInput, 10);
|
|
||||||
const fontSize = Number.isNaN(parsed)
|
|
||||||
? element.fontSize
|
|
||||||
: clamp({ value: parsed, min: 8, max: 300 });
|
|
||||||
setFontSizeInput(fontSize.toString());
|
|
||||||
editor.timeline.updateTextElement({
|
|
||||||
trackId,
|
|
||||||
elementId: element.id,
|
|
||||||
updates: { fontSize },
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleOpacityChange = ({ value }: { value: string }) => {
|
|
||||||
setOpacityInput(value);
|
|
||||||
|
|
||||||
if (value.trim() !== "") {
|
|
||||||
const parsed = parseInt(value, 10);
|
|
||||||
const opacityPercent = Number.isNaN(parsed)
|
|
||||||
? Math.round(element.opacity * 100)
|
|
||||||
: clamp({ value: parsed, min: 0, max: 100 });
|
|
||||||
editor.timeline.updateTextElement({
|
|
||||||
trackId,
|
|
||||||
elementId: element.id,
|
|
||||||
updates: { opacity: opacityPercent / 100 },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleOpacityBlur = () => {
|
|
||||||
const parsed = parseInt(opacityInput, 10);
|
|
||||||
const opacityPercent = Number.isNaN(parsed)
|
|
||||||
? Math.round(element.opacity * 100)
|
|
||||||
: clamp({ value: parsed, min: 0, max: 100 });
|
|
||||||
setOpacityInput(opacityPercent.toString());
|
|
||||||
editor.timeline.updateTextElement({
|
|
||||||
trackId,
|
|
||||||
elementId: element.id,
|
|
||||||
updates: { opacity: opacityPercent / 100 },
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleColorChange = ({ color }: { color: string }) => {
|
|
||||||
if (color !== "transparent") {
|
|
||||||
lastSelectedColor.current = color;
|
|
||||||
}
|
|
||||||
editor.timeline.updateTextElement({
|
|
||||||
trackId,
|
|
||||||
elementId: element.id,
|
|
||||||
updates: { backgroundColor: color },
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleTransparentToggle = ({
|
|
||||||
isTransparent,
|
|
||||||
}: {
|
|
||||||
isTransparent: boolean;
|
|
||||||
}) => {
|
|
||||||
const newColor = isTransparent ? "transparent" : lastSelectedColor.current;
|
|
||||||
editor.timeline.updateTextElement({
|
|
||||||
trackId,
|
|
||||||
elementId: element.id,
|
|
||||||
updates: { backgroundColor: newColor },
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PanelBaseView
|
<div className="flex h-full flex-col">
|
||||||
defaultTab="transform"
|
<ContentSection element={element} trackId={trackId} />
|
||||||
value={activeTab}
|
<TransformSection element={element} trackId={trackId} />
|
||||||
onValueChange={(v) => {
|
<BlendingSection element={element} trackId={trackId} />
|
||||||
if (isTextPropertiesTab(v)) setActiveTab(v);
|
<FontSection element={element} trackId={trackId} />
|
||||||
}}
|
<ColorSection element={element} trackId={trackId} />
|
||||||
ref={containerRef}
|
</div>
|
||||||
tabs={TEXT_PROPERTIES_TABS.map((t) => ({
|
);
|
||||||
value: t.value,
|
}
|
||||||
label: t.label,
|
|
||||||
content:
|
function ContentSection({
|
||||||
t.value === "transform" ? (
|
element,
|
||||||
<div className="space-y-6"></div>
|
trackId,
|
||||||
) : (
|
}: {
|
||||||
<div className="space-y-6">
|
element: TextElement;
|
||||||
<Textarea
|
trackId: string;
|
||||||
placeholder="Name"
|
}) {
|
||||||
defaultValue={element.content}
|
const editor = useEditor();
|
||||||
className="bg-panel-accent min-h-20"
|
|
||||||
onChange={(e) =>
|
const content = usePropertyDraft({
|
||||||
editor.timeline.updateTextElement({
|
displayValue: element.content,
|
||||||
|
parse: (input) => input,
|
||||||
|
onPreview: (value) =>
|
||||||
|
editor.timeline.previewElements({
|
||||||
|
updates: [
|
||||||
|
{ trackId, elementId: element.id, updates: { content: value } },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
onCommit: () => editor.timeline.commitPreview(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Section collapsible sectionKey="text:content" hasBorderTop={false}>
|
||||||
|
<SectionHeader title="Content" />
|
||||||
|
<SectionContent>
|
||||||
|
<Textarea
|
||||||
|
placeholder="Name"
|
||||||
|
value={content.displayValue}
|
||||||
|
className="min-h-20"
|
||||||
|
onFocus={content.onFocus}
|
||||||
|
onChange={content.onChange}
|
||||||
|
onBlur={content.onBlur}
|
||||||
|
/>
|
||||||
|
</SectionContent>
|
||||||
|
</Section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function FontSection({
|
||||||
|
element,
|
||||||
|
trackId,
|
||||||
|
}: {
|
||||||
|
element: TextElement;
|
||||||
|
trackId: string;
|
||||||
|
}) {
|
||||||
|
const editor = useEditor();
|
||||||
|
|
||||||
|
const fontSize = usePropertyDraft({
|
||||||
|
displayValue: element.fontSize.toString(),
|
||||||
|
parse: (input) => {
|
||||||
|
const parsed = parseFloat(input);
|
||||||
|
if (Number.isNaN(parsed)) return null;
|
||||||
|
return clamp({ value: parsed, min: MIN_FONT_SIZE, max: MAX_FONT_SIZE });
|
||||||
|
},
|
||||||
|
onPreview: (value) =>
|
||||||
|
editor.timeline.previewElements({
|
||||||
|
updates: [
|
||||||
|
{ trackId, elementId: element.id, updates: { fontSize: value } },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
onCommit: () => editor.timeline.commitPreview(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Section collapsible sectionKey="text:font">
|
||||||
|
<SectionHeader title="Font" />
|
||||||
|
<SectionContent>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<FontPicker
|
||||||
|
defaultValue={element.fontFamily}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
editor.timeline.updateElements({
|
||||||
|
updates: [
|
||||||
|
{
|
||||||
trackId,
|
trackId,
|
||||||
elementId: element.id,
|
elementId: element.id,
|
||||||
updates: { content: e.target.value },
|
updates: { fontFamily: value },
|
||||||
})
|
},
|
||||||
}
|
],
|
||||||
/>
|
})
|
||||||
<PropertyItem direction="column">
|
}
|
||||||
<PropertyItemLabel>Font</PropertyItemLabel>
|
/>
|
||||||
<PropertyItemValue>
|
<Select value={element.fontWeight}>
|
||||||
<FontPicker
|
<SelectTrigger className="w-full" icon={<OcFontWeightIcon />}>
|
||||||
defaultValue={element.fontFamily}
|
<SelectValue placeholder="Select weight" />
|
||||||
onValueChange={(value: FontFamily) =>
|
</SelectTrigger>
|
||||||
editor.timeline.updateTextElement({
|
<SelectContent>
|
||||||
trackId,
|
<SelectItem value="100">Thin</SelectItem>
|
||||||
elementId: element.id,
|
<SelectItem value="200">Extra Light</SelectItem>
|
||||||
updates: { fontFamily: value },
|
<SelectItem value="300">Light</SelectItem>
|
||||||
})
|
<SelectItem value="400">Normal</SelectItem>
|
||||||
}
|
<SelectItem value="500">Medium</SelectItem>
|
||||||
/>
|
<SelectItem value="600">Semi Bold</SelectItem>
|
||||||
</PropertyItemValue>
|
<SelectItem value="700">Bold</SelectItem>
|
||||||
</PropertyItem>
|
<SelectItem value="800">Extra Bold</SelectItem>
|
||||||
<PropertyItem direction="column">
|
<SelectItem value="900">Black</SelectItem>
|
||||||
<PropertyItemLabel>Style</PropertyItemLabel>
|
</SelectContent>
|
||||||
<PropertyItemValue>
|
</Select>
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Button
|
|
||||||
variant={
|
|
||||||
element.fontWeight === "bold" ? "default" : "outline"
|
|
||||||
}
|
|
||||||
size="sm"
|
|
||||||
onClick={() =>
|
|
||||||
editor.timeline.updateTextElement({
|
|
||||||
trackId,
|
|
||||||
elementId: element.id,
|
|
||||||
updates: {
|
|
||||||
fontWeight:
|
|
||||||
element.fontWeight === "bold" ? "normal" : "bold",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
className="h-8 px-3 font-bold"
|
|
||||||
>
|
|
||||||
B
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant={
|
|
||||||
element.fontStyle === "italic" ? "default" : "outline"
|
|
||||||
}
|
|
||||||
size="sm"
|
|
||||||
onClick={() =>
|
|
||||||
editor.timeline.updateTextElement({
|
|
||||||
trackId,
|
|
||||||
elementId: element.id,
|
|
||||||
updates: {
|
|
||||||
fontStyle:
|
|
||||||
element.fontStyle === "italic"
|
|
||||||
? "normal"
|
|
||||||
: "italic",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
className="h-8 px-3 italic"
|
|
||||||
>
|
|
||||||
I
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant={
|
|
||||||
element.textDecoration === "underline"
|
|
||||||
? "default"
|
|
||||||
: "outline"
|
|
||||||
}
|
|
||||||
size="sm"
|
|
||||||
onClick={() =>
|
|
||||||
editor.timeline.updateTextElement({
|
|
||||||
trackId,
|
|
||||||
elementId: element.id,
|
|
||||||
updates: {
|
|
||||||
textDecoration:
|
|
||||||
element.textDecoration === "underline"
|
|
||||||
? "none"
|
|
||||||
: "underline",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
className="h-8 px-3 underline"
|
|
||||||
>
|
|
||||||
U
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant={
|
|
||||||
element.textDecoration === "line-through"
|
|
||||||
? "default"
|
|
||||||
: "outline"
|
|
||||||
}
|
|
||||||
size="sm"
|
|
||||||
onClick={() =>
|
|
||||||
editor.timeline.updateTextElement({
|
|
||||||
trackId,
|
|
||||||
elementId: element.id,
|
|
||||||
updates: {
|
|
||||||
textDecoration:
|
|
||||||
element.textDecoration === "line-through"
|
|
||||||
? "none"
|
|
||||||
: "line-through",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
className="h-8 px-3 line-through"
|
|
||||||
>
|
|
||||||
S
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</PropertyItemValue>
|
|
||||||
</PropertyItem>
|
|
||||||
<PropertyItem direction="column">
|
|
||||||
<PropertyItemLabel>Font size</PropertyItemLabel>
|
|
||||||
<PropertyItemValue>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Slider
|
|
||||||
value={[element.fontSize]}
|
|
||||||
min={8}
|
|
||||||
max={300}
|
|
||||||
step={1}
|
|
||||||
onValueChange={([value]) => {
|
|
||||||
editor.timeline.updateTextElement({
|
|
||||||
trackId,
|
|
||||||
elementId: element.id,
|
|
||||||
updates: { fontSize: value },
|
|
||||||
});
|
|
||||||
setFontSizeInput(value.toString());
|
|
||||||
}}
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
value={fontSizeInput}
|
|
||||||
min={8}
|
|
||||||
max={300}
|
|
||||||
onChange={(e) =>
|
|
||||||
handleFontSizeChange({ value: e.target.value })
|
|
||||||
}
|
|
||||||
onBlur={handleFontSizeBlur}
|
|
||||||
className="bg-panel-accent h-7 w-12 [appearance:textfield] rounded-sm px-2 text-center !text-xs [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</PropertyItemValue>
|
|
||||||
</PropertyItem>
|
|
||||||
<PropertyItem direction="column">
|
|
||||||
<PropertyItemLabel>Color</PropertyItemLabel>
|
|
||||||
<PropertyItemValue>
|
|
||||||
<ColorPicker
|
|
||||||
value={uppercase({
|
|
||||||
string: (element.color || "FFFFFF").replace("#", ""),
|
|
||||||
})}
|
|
||||||
onChange={(color) => {
|
|
||||||
editor.timeline.updateTextElement({
|
|
||||||
trackId,
|
|
||||||
elementId: element.id,
|
|
||||||
updates: { color: `#${color}` },
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
containerRef={containerRef}
|
|
||||||
/>
|
|
||||||
</PropertyItemValue>
|
|
||||||
</PropertyItem>
|
|
||||||
<PropertyItem direction="column">
|
|
||||||
<PropertyItemLabel>Opacity</PropertyItemLabel>
|
|
||||||
<PropertyItemValue>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Slider
|
|
||||||
value={[element.opacity * 100]}
|
|
||||||
min={0}
|
|
||||||
max={100}
|
|
||||||
step={1}
|
|
||||||
onValueChange={([value]) => {
|
|
||||||
editor.timeline.updateTextElement({
|
|
||||||
trackId,
|
|
||||||
elementId: element.id,
|
|
||||||
updates: { opacity: value / 100 },
|
|
||||||
});
|
|
||||||
setOpacityInput(value.toString());
|
|
||||||
}}
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
value={opacityInput}
|
|
||||||
min={0}
|
|
||||||
max={100}
|
|
||||||
onChange={(e) =>
|
|
||||||
handleOpacityChange({ value: e.target.value })
|
|
||||||
}
|
|
||||||
onBlur={handleOpacityBlur}
|
|
||||||
className="bg-panel-accent h-7 w-12 [appearance:textfield] rounded-sm text-center !text-xs [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</PropertyItemValue>
|
|
||||||
</PropertyItem>
|
|
||||||
<PropertyItem direction="column">
|
|
||||||
<PropertyItemLabel>Background</PropertyItemLabel>
|
|
||||||
<PropertyItemValue>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<ColorPicker
|
|
||||||
value={capitalizeFirstLetter({
|
|
||||||
string:
|
|
||||||
element.backgroundColor === "transparent"
|
|
||||||
? lastSelectedColor.current.replace("#", "")
|
|
||||||
: element.backgroundColor.replace("#", ""),
|
|
||||||
})}
|
|
||||||
onChange={(color) =>
|
|
||||||
handleColorChange({ color: `#${color}` })
|
|
||||||
}
|
|
||||||
containerRef={containerRef}
|
|
||||||
className={
|
|
||||||
element.backgroundColor === "transparent"
|
|
||||||
? "pointer-events-none opacity-50"
|
|
||||||
: ""
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Tooltip>
|
<Label>Font size</Label>
|
||||||
<TooltipTrigger asChild>
|
<div className="flex items-center gap-2">
|
||||||
<Button
|
<Slider
|
||||||
variant="outline"
|
value={[element.fontSize]}
|
||||||
size="icon"
|
min={MIN_FONT_SIZE}
|
||||||
onClick={() =>
|
max={MAX_FONT_SIZE}
|
||||||
handleTransparentToggle({
|
step={1}
|
||||||
isTransparent:
|
onValueChange={([value]) =>
|
||||||
element.backgroundColor !== "transparent",
|
editor.timeline.previewElements({
|
||||||
})
|
updates: [
|
||||||
}
|
{
|
||||||
className="bg-panel-accent size-9 overflow-hidden rounded-full p-0"
|
trackId,
|
||||||
>
|
elementId: element.id,
|
||||||
<HugeiconsIcon
|
updates: { fontSize: value },
|
||||||
icon={LayoutGridIcon}
|
},
|
||||||
className={cn(
|
],
|
||||||
"text-foreground",
|
})
|
||||||
element.backgroundColor === "transparent" &&
|
}
|
||||||
"text-primary",
|
onValueCommit={() => editor.timeline.commitPreview()}
|
||||||
)}
|
className="w-full"
|
||||||
/>
|
/>
|
||||||
</Button>
|
<NumberField
|
||||||
</TooltipTrigger>
|
className="w-18 shrink-0"
|
||||||
<TooltipContent>Transparent background</TooltipContent>
|
value={fontSize.displayValue}
|
||||||
</Tooltip>
|
min={MIN_FONT_SIZE}
|
||||||
</div>
|
max={MAX_FONT_SIZE}
|
||||||
</PropertyItemValue>
|
onFocus={fontSize.onFocus}
|
||||||
</PropertyItem>
|
onChange={fontSize.onChange}
|
||||||
</div>
|
onBlur={fontSize.onBlur}
|
||||||
),
|
onReset={() =>
|
||||||
}))}
|
editor.timeline.updateElements({
|
||||||
/>
|
updates: [
|
||||||
|
{
|
||||||
|
trackId,
|
||||||
|
elementId: element.id,
|
||||||
|
updates: {
|
||||||
|
fontSize: DEFAULT_TEXT_ELEMENT.fontSize,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
isDefault={element.fontSize === DEFAULT_TEXT_ELEMENT.fontSize}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SectionContent>
|
||||||
|
</Section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ColorSection({
|
||||||
|
element,
|
||||||
|
trackId,
|
||||||
|
}: {
|
||||||
|
element: TextElement;
|
||||||
|
trackId: string;
|
||||||
|
}) {
|
||||||
|
const editor = useEditor();
|
||||||
|
const lastSelectedColor = useRef(DEFAULT_COLOR);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Section collapsible sectionKey="text:color" hasBorderBottom={false}>
|
||||||
|
<SectionHeader title="Color" />
|
||||||
|
<SectionContent>
|
||||||
|
<div className="flex flex-col gap-6">
|
||||||
|
<ColorPicker
|
||||||
|
value={uppercase({
|
||||||
|
string: (element.color || "FFFFFF").replace("#", ""),
|
||||||
|
})}
|
||||||
|
onChange={(color) =>
|
||||||
|
editor.timeline.previewElements({
|
||||||
|
updates: [
|
||||||
|
{
|
||||||
|
trackId,
|
||||||
|
elementId: element.id,
|
||||||
|
updates: { color: `#${color}` },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
onChangeEnd={() => editor.timeline.commitPreview()}
|
||||||
|
/>
|
||||||
|
<ColorPicker
|
||||||
|
value={
|
||||||
|
element.backgroundColor === "transparent"
|
||||||
|
? lastSelectedColor.current.replace("#", "")
|
||||||
|
: element.backgroundColor.replace("#", "")
|
||||||
|
}
|
||||||
|
onChange={(color) => {
|
||||||
|
const hexColor = `#${color}`;
|
||||||
|
if (color !== "transparent") {
|
||||||
|
lastSelectedColor.current = hexColor;
|
||||||
|
}
|
||||||
|
editor.timeline.previewElements({
|
||||||
|
updates: [
|
||||||
|
{
|
||||||
|
trackId,
|
||||||
|
elementId: element.id,
|
||||||
|
updates: { backgroundColor: hexColor },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
onChangeEnd={() => editor.timeline.commitPreview()}
|
||||||
|
className={
|
||||||
|
element.backgroundColor === "transparent"
|
||||||
|
? "pointer-events-none opacity-50"
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SectionContent>
|
||||||
|
</Section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,416 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import type { EditorCore } from "@/core";
|
||||||
|
import { useEditor } from "@/hooks/use-editor";
|
||||||
|
import type { BookmarkDragState } from "@/hooks/timeline/use-bookmark-drag";
|
||||||
|
import { BOOKMARK_TIME_EPSILON } from "@/lib/timeline/bookmarks";
|
||||||
|
import {
|
||||||
|
DEFAULT_BOOKMARK_COLOR,
|
||||||
|
TIMELINE_CONSTANTS,
|
||||||
|
} from "@/constants/timeline-constants";
|
||||||
|
import { DEFAULT_FPS } from "@/constants/project-constants";
|
||||||
|
import { getSnappedSeekTime } from "@/lib/time";
|
||||||
|
import {
|
||||||
|
ArrowTurnBackwardIcon,
|
||||||
|
Delete02Icon,
|
||||||
|
} from "@hugeicons/core-free-icons";
|
||||||
|
import { HugeiconsIcon } from "@hugeicons/react";
|
||||||
|
import type { Bookmark } from "@/types/timeline";
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverAnchor,
|
||||||
|
PopoverContent,
|
||||||
|
} from "@/components/ui/popover";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { ColorPicker } from "@/components/ui/color-picker";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { uppercase } from "@/utils/string";
|
||||||
|
import { clamp } from "@/utils/math";
|
||||||
|
|
||||||
|
const PIXELS_PER_SECOND = TIMELINE_CONSTANTS.PIXELS_PER_SECOND;
|
||||||
|
const MIN_BOOKMARK_WIDTH_PX = 2;
|
||||||
|
const BOOKMARK_MARKER_WIDTH_PX = 12;
|
||||||
|
const BOOKMARK_MARKER_HEIGHT_PX = 15;
|
||||||
|
const BOOKMARK_HALF_WIDTH_PX = BOOKMARK_MARKER_WIDTH_PX / 2;
|
||||||
|
const BOOKMARK_MARKER_CLIP_PATH =
|
||||||
|
"polygon(50% 100%, 12% 72%, 12% 10%, 88% 10%, 88% 72%)";
|
||||||
|
|
||||||
|
function seekToBookmarkTime({
|
||||||
|
editor,
|
||||||
|
time,
|
||||||
|
}: {
|
||||||
|
editor: EditorCore;
|
||||||
|
time: number;
|
||||||
|
}) {
|
||||||
|
const activeProject = editor.project.getActive();
|
||||||
|
const duration = editor.timeline.getTotalDuration();
|
||||||
|
const fps = activeProject?.settings.fps ?? DEFAULT_FPS;
|
||||||
|
const snappedTime = getSnappedSeekTime({ rawTime: time, duration, fps });
|
||||||
|
editor.playback.seek({ time: snappedTime });
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TimelineBookmarksRowProps {
|
||||||
|
zoomLevel: number;
|
||||||
|
dynamicTimelineWidth: number;
|
||||||
|
dragState: BookmarkDragState;
|
||||||
|
onBookmarkMouseDown: (params: {
|
||||||
|
event: React.MouseEvent;
|
||||||
|
bookmark: Bookmark;
|
||||||
|
}) => void;
|
||||||
|
handleWheel: (event: React.WheelEvent) => void;
|
||||||
|
handleTimelineContentClick: (event: React.MouseEvent) => void;
|
||||||
|
handleRulerTrackingMouseDown: (event: React.MouseEvent) => void;
|
||||||
|
handleRulerMouseDown: (event: React.MouseEvent) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TimelineBookmarksRow({
|
||||||
|
zoomLevel,
|
||||||
|
dynamicTimelineWidth,
|
||||||
|
dragState,
|
||||||
|
onBookmarkMouseDown,
|
||||||
|
handleWheel,
|
||||||
|
handleTimelineContentClick,
|
||||||
|
handleRulerTrackingMouseDown,
|
||||||
|
handleRulerMouseDown,
|
||||||
|
}: TimelineBookmarksRowProps) {
|
||||||
|
const editor = useEditor();
|
||||||
|
const activeScene = editor.scenes.getActiveScene();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative h-4 flex-1 overflow-hidden">
|
||||||
|
<button
|
||||||
|
className="relative h-4 w-full cursor-default select-none border-0 bg-transparent p-0"
|
||||||
|
style={{
|
||||||
|
width: `${dynamicTimelineWidth}px`,
|
||||||
|
}}
|
||||||
|
aria-label="Timeline ruler"
|
||||||
|
type="button"
|
||||||
|
onWheel={handleWheel}
|
||||||
|
onClick={(event) => {
|
||||||
|
if (!event.currentTarget.contains(event.target as Node)) return;
|
||||||
|
handleTimelineContentClick(event);
|
||||||
|
}}
|
||||||
|
onMouseDown={(event) => {
|
||||||
|
if (!event.currentTarget.contains(event.target as Node)) return;
|
||||||
|
handleRulerMouseDown(event);
|
||||||
|
handleRulerTrackingMouseDown(event);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{activeScene.bookmarks.map((bookmark) => (
|
||||||
|
<TimelineBookmark
|
||||||
|
key={`bookmark-${bookmark.time}`}
|
||||||
|
bookmark={bookmark}
|
||||||
|
zoomLevel={zoomLevel}
|
||||||
|
dragState={dragState}
|
||||||
|
onBookmarkMouseDown={onBookmarkMouseDown}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TimelineBookmark({
|
||||||
|
bookmark,
|
||||||
|
zoomLevel,
|
||||||
|
dragState,
|
||||||
|
onBookmarkMouseDown,
|
||||||
|
}: {
|
||||||
|
bookmark: Bookmark;
|
||||||
|
zoomLevel: number;
|
||||||
|
dragState: BookmarkDragState;
|
||||||
|
onBookmarkMouseDown: (params: {
|
||||||
|
event: React.MouseEvent;
|
||||||
|
bookmark: Bookmark;
|
||||||
|
}) => void;
|
||||||
|
}) {
|
||||||
|
const editor = useEditor();
|
||||||
|
const duration = editor.timeline.getTotalDuration();
|
||||||
|
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||||
|
|
||||||
|
const isDragging =
|
||||||
|
dragState.isDragging &&
|
||||||
|
dragState.bookmarkTime !== null &&
|
||||||
|
Math.abs(dragState.bookmarkTime - bookmark.time) < BOOKMARK_TIME_EPSILON;
|
||||||
|
|
||||||
|
const displayTime = isDragging ? dragState.currentTime : bookmark.time;
|
||||||
|
const time = bookmark.time;
|
||||||
|
const bookmarkDuration = bookmark.duration ?? 0;
|
||||||
|
const durationWidth =
|
||||||
|
bookmarkDuration > 0 ? bookmarkDuration * PIXELS_PER_SECOND * zoomLevel : 0;
|
||||||
|
const hasDurationRange = durationWidth > MIN_BOOKMARK_WIDTH_PX;
|
||||||
|
const bookmarkWidth = BOOKMARK_MARKER_WIDTH_PX + Math.max(durationWidth, 0);
|
||||||
|
const left = displayTime * PIXELS_PER_SECOND * zoomLevel;
|
||||||
|
const bookmarkLeft = left - BOOKMARK_HALF_WIDTH_PX;
|
||||||
|
const rightHalfLeft = BOOKMARK_HALF_WIDTH_PX + Math.max(durationWidth, 0);
|
||||||
|
const iconColor = bookmark.color ?? DEFAULT_BOOKMARK_COLOR;
|
||||||
|
|
||||||
|
const handleSeek = () => seekToBookmarkTime({ editor, time });
|
||||||
|
|
||||||
|
const handleClick = (event: React.MouseEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
if (event.detail === 2) {
|
||||||
|
setIsPopoverOpen(true);
|
||||||
|
} else {
|
||||||
|
handleSeek();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = (event: React.KeyboardEvent) => {
|
||||||
|
if (event.key !== "Enter" && event.key !== " ") return;
|
||||||
|
event.preventDefault();
|
||||||
|
handleSeek();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseDown = (event: React.MouseEvent) => {
|
||||||
|
onBookmarkMouseDown({ event, bookmark });
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
|
||||||
|
<PopoverAnchor asChild>
|
||||||
|
<button
|
||||||
|
className={`absolute top-0 h-full min-w-0.5 border-0 bg-transparent p-0 ${isDragging ? "cursor-grabbing" : "cursor-grab"}`}
|
||||||
|
style={{
|
||||||
|
left: `${bookmarkLeft}px`,
|
||||||
|
width: `${bookmarkWidth}px`,
|
||||||
|
}}
|
||||||
|
aria-label={`Bookmark at ${time.toFixed(1)}s`}
|
||||||
|
type="button"
|
||||||
|
onMouseDown={handleMouseDown}
|
||||||
|
onClick={handleClick}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
>
|
||||||
|
{hasDurationRange ? (
|
||||||
|
<div
|
||||||
|
className="absolute opacity-30"
|
||||||
|
style={{
|
||||||
|
top: 1.5,
|
||||||
|
height: BOOKMARK_MARKER_HEIGHT_PX - 2.5,
|
||||||
|
left: BOOKMARK_HALF_WIDTH_PX,
|
||||||
|
width: durationWidth,
|
||||||
|
backgroundColor: iconColor,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
<div
|
||||||
|
className="absolute top-0 overflow-hidden"
|
||||||
|
style={{
|
||||||
|
left: 0,
|
||||||
|
width: BOOKMARK_HALF_WIDTH_PX,
|
||||||
|
height: BOOKMARK_MARKER_HEIGHT_PX,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="absolute inset-0"
|
||||||
|
style={{
|
||||||
|
width: BOOKMARK_MARKER_WIDTH_PX,
|
||||||
|
height: BOOKMARK_MARKER_HEIGHT_PX,
|
||||||
|
clipPath: BOOKMARK_MARKER_CLIP_PATH,
|
||||||
|
backgroundColor: "hsl(var(--background))",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="absolute"
|
||||||
|
style={{
|
||||||
|
top: 1,
|
||||||
|
left: 1,
|
||||||
|
width: BOOKMARK_MARKER_WIDTH_PX - 2,
|
||||||
|
height: BOOKMARK_MARKER_HEIGHT_PX - 2,
|
||||||
|
clipPath: BOOKMARK_MARKER_CLIP_PATH,
|
||||||
|
backgroundColor: iconColor,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="absolute top-0 overflow-hidden"
|
||||||
|
style={{
|
||||||
|
left: rightHalfLeft,
|
||||||
|
width: BOOKMARK_HALF_WIDTH_PX,
|
||||||
|
height: BOOKMARK_MARKER_HEIGHT_PX,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="absolute top-0"
|
||||||
|
style={{
|
||||||
|
left: -BOOKMARK_HALF_WIDTH_PX,
|
||||||
|
width: BOOKMARK_MARKER_WIDTH_PX,
|
||||||
|
height: BOOKMARK_MARKER_HEIGHT_PX,
|
||||||
|
clipPath: BOOKMARK_MARKER_CLIP_PATH,
|
||||||
|
backgroundColor: "hsl(var(--background))",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="absolute"
|
||||||
|
style={{
|
||||||
|
top: 1,
|
||||||
|
left: 1 - BOOKMARK_HALF_WIDTH_PX,
|
||||||
|
width: BOOKMARK_MARKER_WIDTH_PX - 2,
|
||||||
|
height: BOOKMARK_MARKER_HEIGHT_PX - 2,
|
||||||
|
clipPath: BOOKMARK_MARKER_CLIP_PATH,
|
||||||
|
backgroundColor: iconColor,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</PopoverAnchor>
|
||||||
|
<PopoverContent
|
||||||
|
className="w-64 flex flex-col gap-3 p-3"
|
||||||
|
align="start"
|
||||||
|
side="bottom"
|
||||||
|
sideOffset={8}
|
||||||
|
onOpenAutoFocus={(event) => event.preventDefault()}
|
||||||
|
>
|
||||||
|
<BookmarkPopoverContent
|
||||||
|
bookmark={bookmark}
|
||||||
|
time={time}
|
||||||
|
timelineDuration={duration}
|
||||||
|
onPopoverClose={() => setIsPopoverOpen(false)}
|
||||||
|
/>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function BookmarkPopoverContent({
|
||||||
|
bookmark,
|
||||||
|
time,
|
||||||
|
timelineDuration,
|
||||||
|
onPopoverClose,
|
||||||
|
}: {
|
||||||
|
bookmark: Bookmark;
|
||||||
|
time: number;
|
||||||
|
timelineDuration: number;
|
||||||
|
onPopoverClose: () => void;
|
||||||
|
}) {
|
||||||
|
const editor = useEditor();
|
||||||
|
const [draftColorHex, setDraftColorHex] = useState(
|
||||||
|
(bookmark.color ?? DEFAULT_BOOKMARK_COLOR).replace("#", "").toUpperCase(),
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setDraftColorHex(
|
||||||
|
(bookmark.color ?? DEFAULT_BOOKMARK_COLOR).replace("#", "").toUpperCase(),
|
||||||
|
);
|
||||||
|
}, [bookmark.color]);
|
||||||
|
|
||||||
|
const handleRemove = () => {
|
||||||
|
editor.scenes.removeBookmark({ time });
|
||||||
|
onPopoverClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdate = ({
|
||||||
|
note,
|
||||||
|
color,
|
||||||
|
duration,
|
||||||
|
}: Partial<{ note: string; color: string; duration: number }>) => {
|
||||||
|
const updates: Partial<{ note: string; color: string; duration: number }> =
|
||||||
|
{};
|
||||||
|
if (note !== undefined && note !== bookmark.note) updates.note = note;
|
||||||
|
if (
|
||||||
|
color !== undefined &&
|
||||||
|
color.toUpperCase() !== (bookmark.color ?? "").toUpperCase()
|
||||||
|
) {
|
||||||
|
updates.color = color;
|
||||||
|
}
|
||||||
|
if (duration !== undefined && duration !== bookmark.duration) {
|
||||||
|
updates.duration = duration;
|
||||||
|
}
|
||||||
|
if (Object.keys(updates).length === 0) return;
|
||||||
|
editor.scenes.updateBookmark({ time, updates });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Label className="text-xs">Note</Label>
|
||||||
|
<Input
|
||||||
|
placeholder="Add a note..."
|
||||||
|
value={bookmark.note ?? ""}
|
||||||
|
onChange={(event) => handleUpdate({ note: event.target.value })}
|
||||||
|
className="h-8 text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Label className="text-xs">Color</Label>
|
||||||
|
<div className="relative">
|
||||||
|
<ColorPicker
|
||||||
|
value={uppercase({ string: draftColorHex })}
|
||||||
|
onChange={(color) => setDraftColorHex(uppercase({ string: color }))}
|
||||||
|
onChangeEnd={(color) =>
|
||||||
|
handleUpdate({ color: `#${uppercase({ string: color })}` })
|
||||||
|
}
|
||||||
|
className="bg-background border"
|
||||||
|
/>
|
||||||
|
{bookmark.color &&
|
||||||
|
bookmark.color.replace(/^#/, "").toUpperCase() !==
|
||||||
|
DEFAULT_BOOKMARK_COLOR.replace(/^#/, "").toUpperCase() && (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="text"
|
||||||
|
size="text"
|
||||||
|
aria-label="Reset to default color"
|
||||||
|
className="absolute top-1/2 right-1 -translate-y-1/2 mr-1"
|
||||||
|
onClick={() =>
|
||||||
|
editor.scenes.updateBookmark({
|
||||||
|
time,
|
||||||
|
updates: { color: undefined },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<HugeiconsIcon
|
||||||
|
icon={ArrowTurnBackwardIcon}
|
||||||
|
className="!size-3.5"
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Label className="text-xs">Duration</Label>
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min={0}
|
||||||
|
step={0.1}
|
||||||
|
value={bookmark.duration ?? 0}
|
||||||
|
onChange={(event) => {
|
||||||
|
const parsed = parseFloat(event.target.value);
|
||||||
|
const value = Number.isNaN(parsed)
|
||||||
|
? 0
|
||||||
|
: clamp({
|
||||||
|
value: parsed,
|
||||||
|
min: 0,
|
||||||
|
max: Math.max(0, timelineDuration - time),
|
||||||
|
});
|
||||||
|
handleUpdate({ duration: value });
|
||||||
|
}}
|
||||||
|
className="h-8 text-sm"
|
||||||
|
containerClassName="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="text-destructive hover:bg-destructive/10"
|
||||||
|
onClick={handleRemove}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === "Enter" || event.key === " ") {
|
||||||
|
handleRemove();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
aria-label="delete bookmark"
|
||||||
|
>
|
||||||
|
<HugeiconsIcon icon={Delete02Icon} className="!size-3.5" />
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -15,12 +15,12 @@ import {
|
|||||||
ContextMenuContent,
|
ContextMenuContent,
|
||||||
ContextMenuItem,
|
ContextMenuItem,
|
||||||
ContextMenuTrigger,
|
ContextMenuTrigger,
|
||||||
} from "../../ui/context-menu";
|
} from "../../../ui/context-menu";
|
||||||
import { useTimelineZoom } from "@/hooks/timeline/use-timeline-zoom";
|
import { useTimelineZoom } from "@/hooks/timeline/use-timeline-zoom";
|
||||||
import { useState, useRef, useCallback } from "react";
|
import { useState, useRef, useCallback } from "react";
|
||||||
import { TimelineTrackContent } from "./timeline-track";
|
import { TimelineTrackContent } from "./timeline-track";
|
||||||
import { TimelinePlayhead } from "./timeline-playhead";
|
import { TimelinePlayhead } from "./timeline-playhead";
|
||||||
import { SelectionBox } from "../selection-box";
|
import { SelectionBox } from "../../selection-box";
|
||||||
import { useSelectionBox } from "@/hooks/timeline/use-selection-box";
|
import { useSelectionBox } from "@/hooks/timeline/use-selection-box";
|
||||||
import { SnapIndicator } from "./snap-indicator";
|
import { SnapIndicator } from "./snap-indicator";
|
||||||
import type { SnapPoint } from "@/hooks/timeline/use-timeline-snapping";
|
import type { SnapPoint } from "@/hooks/timeline/use-timeline-snapping";
|
||||||
@@ -47,6 +47,8 @@ import { useTimelineSeek } from "@/hooks/timeline/use-timeline-seek";
|
|||||||
import { useTimelineDragDrop } from "@/hooks/timeline/use-timeline-drag-drop";
|
import { useTimelineDragDrop } from "@/hooks/timeline/use-timeline-drag-drop";
|
||||||
import { TimelineRuler } from "./timeline-ruler";
|
import { TimelineRuler } from "./timeline-ruler";
|
||||||
import { TimelineBookmarksRow } from "./bookmarks";
|
import { TimelineBookmarksRow } from "./bookmarks";
|
||||||
|
import { useBookmarkDrag } from "@/hooks/timeline/use-bookmark-drag";
|
||||||
|
import { useEdgeAutoScroll } from "@/hooks/timeline/use-edge-auto-scroll";
|
||||||
import { useTimelineStore } from "@/stores/timeline-store";
|
import { useTimelineStore } from "@/stores/timeline-store";
|
||||||
import { useEditor } from "@/hooks/use-editor";
|
import { useEditor } from "@/hooks/use-editor";
|
||||||
import { useTimelinePlayhead } from "@/hooks/timeline/use-timeline-playhead";
|
import { useTimelinePlayhead } from "@/hooks/timeline/use-timeline-playhead";
|
||||||
@@ -126,6 +128,17 @@ export function Timeline() {
|
|||||||
onSnapPointChange: handleSnapPointChange,
|
onSnapPointChange: handleSnapPointChange,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
dragState: bookmarkDragState,
|
||||||
|
handleBookmarkMouseDown,
|
||||||
|
lastMouseXRef: bookmarkLastMouseXRef,
|
||||||
|
} = useBookmarkDrag({
|
||||||
|
zoomLevel,
|
||||||
|
scrollRef: tracksScrollRef,
|
||||||
|
snappingEnabled,
|
||||||
|
onSnapPointChange: handleSnapPointChange,
|
||||||
|
});
|
||||||
|
|
||||||
const { handleRulerMouseDown: handlePlayheadRulerMouseDown } =
|
const { handleRulerMouseDown: handlePlayheadRulerMouseDown } =
|
||||||
useTimelinePlayhead({
|
useTimelinePlayhead({
|
||||||
zoomLevel,
|
zoomLevel,
|
||||||
@@ -168,10 +181,18 @@ export function Timeline() {
|
|||||||
containerWidth,
|
containerWidth,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEdgeAutoScroll({
|
||||||
|
isActive: bookmarkDragState.isDragging,
|
||||||
|
getMouseClientX: () => bookmarkLastMouseXRef.current,
|
||||||
|
rulerScrollRef: tracksScrollRef,
|
||||||
|
tracksScrollRef,
|
||||||
|
contentWidth: dynamicTimelineWidth,
|
||||||
|
});
|
||||||
|
|
||||||
const showSnapIndicator =
|
const showSnapIndicator =
|
||||||
snappingEnabled &&
|
snappingEnabled &&
|
||||||
currentSnapPoint !== null &&
|
currentSnapPoint !== null &&
|
||||||
(dragState.isDragging || isResizing);
|
(dragState.isDragging || bookmarkDragState.isDragging || isResizing);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
handleTracksMouseDown,
|
handleTracksMouseDown,
|
||||||
@@ -201,7 +222,7 @@ export function Timeline() {
|
|||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
className={
|
className={
|
||||||
"bg-panel relative flex h-full flex-col overflow-hidden rounded-sm"
|
"panel bg-background relative flex h-full flex-col overflow-hidden rounded-sm border"
|
||||||
}
|
}
|
||||||
{...dragProps}
|
{...dragProps}
|
||||||
aria-label="Timeline"
|
aria-label="Timeline"
|
||||||
@@ -226,17 +247,17 @@ export function Timeline() {
|
|||||||
isVisible={showSnapIndicator}
|
isVisible={showSnapIndicator}
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-1 overflow-hidden">
|
<div className="flex flex-1 overflow-hidden">
|
||||||
<div className="bg-panel flex w-28 shrink-0 flex-col border-r">
|
<div className="bg-background flex w-28 shrink-0 flex-col border-r">
|
||||||
<div className="bg-panel flex h-4 items-center justify-between px-3">
|
<div className="bg-background flex h-4 items-center justify-between px-3">
|
||||||
<span className="opacity-0">.</span>
|
<span className="opacity-0">.</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-panel flex h-4 items-center justify-between px-3">
|
<div className="bg-background flex h-4 items-center justify-between px-3">
|
||||||
<span className="opacity-0">.</span>
|
<span className="opacity-0">.</span>
|
||||||
</div>
|
</div>
|
||||||
{tracks.length > 0 && (
|
{tracks.length > 0 && (
|
||||||
<div
|
<div
|
||||||
ref={trackLabelsRef}
|
ref={trackLabelsRef}
|
||||||
className="bg-panel flex-1 overflow-y-auto"
|
className="bg-background flex-1 overflow-y-auto"
|
||||||
style={{ paddingTop: TIMELINE_CONSTANTS.PADDING_TOP_PX }}
|
style={{ paddingTop: TIMELINE_CONSTANTS.PADDING_TOP_PX }}
|
||||||
>
|
>
|
||||||
<ScrollArea className="size-full" ref={trackLabelsScrollRef}>
|
<ScrollArea className="size-full" ref={trackLabelsScrollRef}>
|
||||||
@@ -351,12 +372,13 @@ export function Timeline() {
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
ref={timelineHeaderRef}
|
ref={timelineHeaderRef}
|
||||||
className="bg-panel sticky top-0 z-30 flex flex-col"
|
className="bg-background sticky top-0 flex flex-col"
|
||||||
>
|
>
|
||||||
<TimelineRuler
|
<TimelineRuler
|
||||||
zoomLevel={zoomLevel}
|
zoomLevel={zoomLevel}
|
||||||
dynamicTimelineWidth={dynamicTimelineWidth}
|
dynamicTimelineWidth={dynamicTimelineWidth}
|
||||||
rulerRef={rulerRef}
|
rulerRef={rulerRef}
|
||||||
|
tracksScrollRef={tracksScrollRef}
|
||||||
handleWheel={handleWheel}
|
handleWheel={handleWheel}
|
||||||
handleTimelineContentClick={handleRulerClick}
|
handleTimelineContentClick={handleRulerClick}
|
||||||
handleRulerTrackingMouseDown={handleRulerMouseDown}
|
handleRulerTrackingMouseDown={handleRulerMouseDown}
|
||||||
@@ -365,6 +387,8 @@ export function Timeline() {
|
|||||||
<TimelineBookmarksRow
|
<TimelineBookmarksRow
|
||||||
zoomLevel={zoomLevel}
|
zoomLevel={zoomLevel}
|
||||||
dynamicTimelineWidth={dynamicTimelineWidth}
|
dynamicTimelineWidth={dynamicTimelineWidth}
|
||||||
|
dragState={bookmarkDragState}
|
||||||
|
onBookmarkMouseDown={handleBookmarkMouseDown}
|
||||||
handleWheel={handleWheel}
|
handleWheel={handleWheel}
|
||||||
handleTimelineContentClick={handleRulerClick}
|
handleTimelineContentClick={handleRulerClick}
|
||||||
handleRulerTrackingMouseDown={handleRulerMouseDown}
|
handleRulerTrackingMouseDown={handleRulerMouseDown}
|
||||||
@@ -423,12 +447,16 @@ export function Timeline() {
|
|||||||
onResizeStateChange={handleResizeStateChange}
|
onResizeStateChange={handleResizeStateChange}
|
||||||
onElementMouseDown={handleElementMouseDown}
|
onElementMouseDown={handleElementMouseDown}
|
||||||
onElementClick={handleElementClick}
|
onElementClick={handleElementClick}
|
||||||
onTrackMouseDown={handleSelectionMouseDown}
|
onTrackMouseDown={(event) => {
|
||||||
|
handleSelectionMouseDown(event);
|
||||||
|
handleTracksMouseDown(event);
|
||||||
|
}}
|
||||||
|
onTrackClick={handleTracksClick}
|
||||||
shouldIgnoreClick={shouldIgnoreClick}
|
shouldIgnoreClick={shouldIgnoreClick}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</ContextMenuTrigger>
|
</ContextMenuTrigger>
|
||||||
<ContextMenuContent className="z-200 w-40">
|
<ContextMenuContent className="w-40">
|
||||||
<ContextMenuItem
|
<ContextMenuItem
|
||||||
icon={<HugeiconsIcon icon={TaskAdd02Icon} />}
|
icon={<HugeiconsIcon icon={TaskAdd02Icon} />}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
@@ -38,7 +38,7 @@ export function SnapIndicator({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="pointer-events-none absolute z-90"
|
className="pointer-events-none absolute"
|
||||||
style={{
|
style={{
|
||||||
left: `${leftPosition}px`,
|
left: `${leftPosition}px`,
|
||||||
top: topPosition,
|
top: topPosition,
|
||||||
@@ -19,7 +19,7 @@ import {
|
|||||||
ContextMenuItem,
|
ContextMenuItem,
|
||||||
ContextMenuSeparator,
|
ContextMenuSeparator,
|
||||||
ContextMenuTrigger,
|
ContextMenuTrigger,
|
||||||
} from "../../ui/context-menu";
|
} from "../../../ui/context-menu";
|
||||||
import type {
|
import type {
|
||||||
TimelineElement as TimelineElementType,
|
TimelineElement as TimelineElementType,
|
||||||
TimelineTrack,
|
TimelineTrack,
|
||||||
@@ -29,8 +29,8 @@ import type { MediaAsset } from "@/types/assets";
|
|||||||
import { mediaSupportsAudio } from "@/lib/media/media-utils";
|
import { mediaSupportsAudio } from "@/lib/media/media-utils";
|
||||||
import { getActionDefinition, type TAction, invokeAction } from "@/lib/actions";
|
import { getActionDefinition, type TAction, invokeAction } from "@/lib/actions";
|
||||||
import { useElementSelection } from "@/hooks/timeline/element/use-element-selection";
|
import { useElementSelection } from "@/hooks/timeline/element/use-element-selection";
|
||||||
|
import { resolveStickerId } from "@/lib/stickers";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { Filmstrip } from "./filmstrip";
|
|
||||||
import {
|
import {
|
||||||
ScissorIcon,
|
ScissorIcon,
|
||||||
Delete02Icon,
|
Delete02Icon,
|
||||||
@@ -140,9 +140,7 @@ export function TimelineElement({
|
|||||||
<ContextMenu>
|
<ContextMenu>
|
||||||
<ContextMenuTrigger asChild>
|
<ContextMenuTrigger asChild>
|
||||||
<div
|
<div
|
||||||
className={`absolute top-0 h-full select-none ${
|
className={`absolute top-0 h-full select-none`}
|
||||||
isBeingDragged ? "z-30" : "z-10"
|
|
||||||
}`}
|
|
||||||
style={{
|
style={{
|
||||||
left: `${elementLeft}px`,
|
left: `${elementLeft}px`,
|
||||||
width: `${elementWidth}px`,
|
width: `${elementWidth}px`,
|
||||||
@@ -156,18 +154,16 @@ export function TimelineElement({
|
|||||||
element={element}
|
element={element}
|
||||||
track={track}
|
track={track}
|
||||||
isSelected={isSelected}
|
isSelected={isSelected}
|
||||||
isBeingDragged={isBeingDragged}
|
|
||||||
hasAudio={hasAudio}
|
hasAudio={hasAudio}
|
||||||
isMuted={isMuted}
|
isMuted={isMuted}
|
||||||
mediaAssets={mediaAssets}
|
mediaAssets={mediaAssets}
|
||||||
zoomLevel={zoomLevel}
|
|
||||||
onElementClick={onElementClick}
|
onElementClick={onElementClick}
|
||||||
onElementMouseDown={onElementMouseDown}
|
onElementMouseDown={onElementMouseDown}
|
||||||
handleResizeStart={handleResizeStart}
|
handleResizeStart={handleResizeStart}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</ContextMenuTrigger>
|
</ContextMenuTrigger>
|
||||||
<ContextMenuContent className="z-200 w-64">
|
<ContextMenuContent className="w-64">
|
||||||
<ActionMenuItem
|
<ActionMenuItem
|
||||||
action="split"
|
action="split"
|
||||||
icon={<HugeiconsIcon icon={ScissorIcon} />}
|
icon={<HugeiconsIcon icon={ScissorIcon} />}
|
||||||
@@ -229,11 +225,9 @@ function ElementInner({
|
|||||||
element,
|
element,
|
||||||
track,
|
track,
|
||||||
isSelected,
|
isSelected,
|
||||||
isBeingDragged,
|
|
||||||
hasAudio,
|
hasAudio,
|
||||||
isMuted,
|
isMuted,
|
||||||
mediaAssets,
|
mediaAssets,
|
||||||
zoomLevel,
|
|
||||||
onElementClick,
|
onElementClick,
|
||||||
onElementMouseDown,
|
onElementMouseDown,
|
||||||
handleResizeStart,
|
handleResizeStart,
|
||||||
@@ -241,18 +235,16 @@ function ElementInner({
|
|||||||
element: TimelineElementType;
|
element: TimelineElementType;
|
||||||
track: TimelineTrack;
|
track: TimelineTrack;
|
||||||
isSelected: boolean;
|
isSelected: boolean;
|
||||||
isBeingDragged: boolean;
|
|
||||||
hasAudio: boolean;
|
hasAudio: boolean;
|
||||||
isMuted: boolean;
|
isMuted: boolean;
|
||||||
mediaAssets: MediaAsset[];
|
mediaAssets: MediaAsset[];
|
||||||
zoomLevel: number;
|
|
||||||
onElementClick: (e: React.MouseEvent, element: TimelineElementType) => void;
|
onElementClick: (e: React.MouseEvent, element: TimelineElementType) => void;
|
||||||
onElementMouseDown: (
|
onElementMouseDown: (
|
||||||
e: React.MouseEvent,
|
e: React.MouseEvent,
|
||||||
element: TimelineElementType,
|
element: TimelineElementType,
|
||||||
) => void;
|
) => void;
|
||||||
handleResizeStart: (params: {
|
handleResizeStart: (params: {
|
||||||
e: React.MouseEvent;
|
event: React.MouseEvent;
|
||||||
elementId: string;
|
elementId: string;
|
||||||
side: "left" | "right";
|
side: "left" | "right";
|
||||||
}) => void;
|
}) => void;
|
||||||
@@ -263,7 +255,7 @@ function ElementInner({
|
|||||||
{
|
{
|
||||||
type: track.type,
|
type: track.type,
|
||||||
},
|
},
|
||||||
)} ${isBeingDragged ? "z-30" : "z-10"} ${canElementBeHidden(element) && element.hidden ? "opacity-50" : ""}`}
|
)} ${canElementBeHidden(element) && element.hidden ? "opacity-50" : ""}`}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -277,7 +269,6 @@ function ElementInner({
|
|||||||
track={track}
|
track={track}
|
||||||
isSelected={isSelected}
|
isSelected={isSelected}
|
||||||
mediaAssets={mediaAssets}
|
mediaAssets={mediaAssets}
|
||||||
zoomLevel={zoomLevel}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -326,7 +317,7 @@ function ResizeHandle({
|
|||||||
side: "left" | "right";
|
side: "left" | "right";
|
||||||
elementId: string;
|
elementId: string;
|
||||||
handleResizeStart: (params: {
|
handleResizeStart: (params: {
|
||||||
e: React.MouseEvent;
|
event: React.MouseEvent;
|
||||||
elementId: string;
|
elementId: string;
|
||||||
side: "left" | "right";
|
side: "left" | "right";
|
||||||
}) => void;
|
}) => void;
|
||||||
@@ -335,8 +326,8 @@ function ResizeHandle({
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`bg-primary absolute top-0 bottom-0 z-50 flex w-[0.6rem] items-center justify-center ${isLeft ? "left-0 cursor-w-resize" : "right-0 cursor-e-resize"}`}
|
className={`bg-primary absolute top-0 bottom-0 flex w-[0.6rem] items-center justify-center ${isLeft ? "left-0 cursor-w-resize" : "right-0 cursor-e-resize"}`}
|
||||||
onMouseDown={(e) => handleResizeStart({ e, elementId, side })}
|
onMouseDown={(event) => handleResizeStart({ event, elementId, side })}
|
||||||
aria-label={`${isLeft ? "Left" : "Right"} resize handle`}
|
aria-label={`${isLeft ? "Left" : "Right"} resize handle`}
|
||||||
>
|
>
|
||||||
<div className="bg-foreground h-[1.5rem] w-[0.2rem] rounded-full" />
|
<div className="bg-foreground h-[1.5rem] w-[0.2rem] rounded-full" />
|
||||||
@@ -349,13 +340,11 @@ function ElementContent({
|
|||||||
track,
|
track,
|
||||||
isSelected,
|
isSelected,
|
||||||
mediaAssets,
|
mediaAssets,
|
||||||
zoomLevel,
|
|
||||||
}: {
|
}: {
|
||||||
element: TimelineElementType;
|
element: TimelineElementType;
|
||||||
track: TimelineTrack;
|
track: TimelineTrack;
|
||||||
isSelected: boolean;
|
isSelected: boolean;
|
||||||
mediaAssets: MediaAsset[];
|
mediaAssets: MediaAsset[];
|
||||||
zoomLevel: number;
|
|
||||||
}) {
|
}) {
|
||||||
if (element.type === "text") {
|
if (element.type === "text") {
|
||||||
return (
|
return (
|
||||||
@@ -369,7 +358,10 @@ function ElementContent({
|
|||||||
return (
|
return (
|
||||||
<div className="flex size-full items-center gap-2 pl-2">
|
<div className="flex size-full items-center gap-2 pl-2">
|
||||||
<Image
|
<Image
|
||||||
src={`https://api.iconify.design/${element.iconName}.svg?width=20&height=20`}
|
src={resolveStickerId({
|
||||||
|
stickerId: element.stickerId,
|
||||||
|
options: { width: 20, height: 20 },
|
||||||
|
})}
|
||||||
alt={element.name}
|
alt={element.name}
|
||||||
className="size-5 shrink-0"
|
className="size-5 shrink-0"
|
||||||
width={20}
|
width={20}
|
||||||
@@ -421,10 +413,14 @@ function ElementContent({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mediaAsset.type === "image") {
|
if (
|
||||||
|
mediaAsset.type === "image" ||
|
||||||
|
(mediaAsset.type === "video" && mediaAsset.thumbnailUrl)
|
||||||
|
) {
|
||||||
const trackHeight = getTrackHeight({ type: track.type });
|
const trackHeight = getTrackHeight({ type: track.type });
|
||||||
const tileWidth = trackHeight * (16 / 9);
|
const tileWidth = trackHeight * (16 / 9);
|
||||||
const imageUrl = mediaAsset.url;
|
const imageUrl =
|
||||||
|
mediaAsset.type === "image" ? mediaAsset.url : mediaAsset.thumbnailUrl;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex size-full items-center justify-center">
|
<div className="flex size-full items-center justify-center">
|
||||||
@@ -448,29 +444,6 @@ function ElementContent({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mediaAsset.type === "video") {
|
|
||||||
const trackHeight = getTrackHeight({ type: track.type });
|
|
||||||
const trimStart = element.trimStart ?? 0;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex size-full items-center justify-center">
|
|
||||||
<div
|
|
||||||
className={`relative size-full ${isSelected ? "bg-primary" : "bg-transparent"}`}
|
|
||||||
>
|
|
||||||
<Filmstrip
|
|
||||||
mediaId={element.mediaId}
|
|
||||||
file={mediaAsset.file}
|
|
||||||
duration={element.duration}
|
|
||||||
trimStart={trimStart}
|
|
||||||
trackHeight={trackHeight}
|
|
||||||
zoomLevel={zoomLevel}
|
|
||||||
fallbackThumbnailUrl={mediaAsset.thumbnailUrl}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className="text-foreground/80 truncate text-xs">{element.name}</span>
|
<span className="text-foreground/80 truncate text-xs">{element.name}</span>
|
||||||
);
|
);
|
||||||
@@ -72,20 +72,22 @@ export function TimelinePlayhead({
|
|||||||
aria-valuemax={duration}
|
aria-valuemax={duration}
|
||||||
aria-valuenow={playheadPosition}
|
aria-valuenow={playheadPosition}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
className="pointer-events-auto absolute z-60"
|
className="pointer-events-none absolute z-5"
|
||||||
style={{
|
style={{
|
||||||
left: `${leftPosition}px`,
|
left: `${leftPosition}px`,
|
||||||
top: 0,
|
top: 0,
|
||||||
height: `${totalHeight}px`,
|
height: `${totalHeight}px`,
|
||||||
width: "2px",
|
width: "2px",
|
||||||
}}
|
}}
|
||||||
onMouseDown={handlePlayheadMouseDown}
|
|
||||||
onKeyDown={handlePlayheadKeyDown}
|
onKeyDown={handlePlayheadKeyDown}
|
||||||
>
|
>
|
||||||
<div className="bg-foreground absolute left-0 h-full w-0.5 cursor-col-resize" />
|
<div className="bg-foreground pointer-events-none absolute left-0 h-full w-0.5" />
|
||||||
|
|
||||||
<div
|
<button
|
||||||
className={`absolute top-1 left-1/2 size-3 -translate-x-1/2 transform rounded-full border-2 shadow-xs ${isSnappingToPlayhead ? "bg-foreground border-foreground" : "bg-foreground border-foreground/50"}`}
|
type="button"
|
||||||
|
aria-label="Drag playhead"
|
||||||
|
className={`pointer-events-auto absolute top-1 left-1/2 size-3 -translate-x-1/2 transform cursor-col-resize rounded-full border-2 shadow-xs ${isSnappingToPlayhead ? "bg-foreground border-foreground" : "bg-foreground border-foreground/50"}`}
|
||||||
|
onMouseDown={handlePlayheadMouseDown}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -1,14 +1,16 @@
|
|||||||
import type { JSX } from "react";
|
import { type JSX, useLayoutEffect, useRef } from "react";
|
||||||
import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
|
import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
|
||||||
import { DEFAULT_FPS } from "@/constants/project-constants";
|
import { DEFAULT_FPS } from "@/constants/project-constants";
|
||||||
import { useEditor } from "@/hooks/use-editor";
|
import { useEditor } from "@/hooks/use-editor";
|
||||||
import { getRulerConfig, shouldShowLabel } from "@/lib/timeline/ruler-utils";
|
import { getRulerConfig, shouldShowLabel } from "@/lib/timeline/ruler-utils";
|
||||||
|
import { useScrollPosition } from "@/hooks/timeline/use-scroll-position";
|
||||||
import { TimelineTick } from "./timeline-tick";
|
import { TimelineTick } from "./timeline-tick";
|
||||||
|
|
||||||
interface TimelineRulerProps {
|
interface TimelineRulerProps {
|
||||||
zoomLevel: number;
|
zoomLevel: number;
|
||||||
dynamicTimelineWidth: number;
|
dynamicTimelineWidth: number;
|
||||||
rulerRef: React.Ref<HTMLDivElement>;
|
rulerRef: React.Ref<HTMLDivElement>;
|
||||||
|
tracksScrollRef: React.RefObject<HTMLElement | null>;
|
||||||
handleWheel: (e: React.WheelEvent) => void;
|
handleWheel: (e: React.WheelEvent) => void;
|
||||||
handleTimelineContentClick: (e: React.MouseEvent) => void;
|
handleTimelineContentClick: (e: React.MouseEvent) => void;
|
||||||
handleRulerTrackingMouseDown: (e: React.MouseEvent) => void;
|
handleRulerTrackingMouseDown: (e: React.MouseEvent) => void;
|
||||||
@@ -19,6 +21,7 @@ export function TimelineRuler({
|
|||||||
zoomLevel,
|
zoomLevel,
|
||||||
dynamicTimelineWidth,
|
dynamicTimelineWidth,
|
||||||
rulerRef,
|
rulerRef,
|
||||||
|
tracksScrollRef,
|
||||||
handleWheel,
|
handleWheel,
|
||||||
handleTimelineContentClick,
|
handleTimelineContentClick,
|
||||||
handleRulerTrackingMouseDown,
|
handleRulerTrackingMouseDown,
|
||||||
@@ -37,8 +40,47 @@ export function TimelineRuler({
|
|||||||
});
|
});
|
||||||
const tickCount = Math.ceil(effectiveDuration / tickIntervalSeconds) + 1;
|
const tickCount = Math.ceil(effectiveDuration / tickIntervalSeconds) + 1;
|
||||||
|
|
||||||
|
const { scrollLeft, viewportWidth } = useScrollPosition({
|
||||||
|
scrollRef: tracksScrollRef,
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* widens the virtualization buffer during zoom transitions.
|
||||||
|
* useScrollPosition lags one frame behind the scroll adjustment
|
||||||
|
* that useLayoutEffect applies after a zoom change.
|
||||||
|
*/
|
||||||
|
const prevZoomRef = useRef(zoomLevel);
|
||||||
|
const isZoomTransition = zoomLevel !== prevZoomRef.current;
|
||||||
|
const bufferPx = isZoomTransition
|
||||||
|
? Math.max(200, (scrollLeft + viewportWidth) * 0.15)
|
||||||
|
: 200;
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
prevZoomRef.current = zoomLevel;
|
||||||
|
}, [zoomLevel]);
|
||||||
|
|
||||||
|
const visibleStartTime = Math.max(
|
||||||
|
0,
|
||||||
|
(scrollLeft - bufferPx) / pixelsPerSecond,
|
||||||
|
);
|
||||||
|
const visibleEndTime =
|
||||||
|
(scrollLeft + viewportWidth + bufferPx) / pixelsPerSecond;
|
||||||
|
|
||||||
|
const startTickIndex = Math.max(
|
||||||
|
0,
|
||||||
|
Math.floor(visibleStartTime / tickIntervalSeconds),
|
||||||
|
);
|
||||||
|
const endTickIndex = Math.min(
|
||||||
|
tickCount - 1,
|
||||||
|
Math.ceil(visibleEndTime / tickIntervalSeconds),
|
||||||
|
);
|
||||||
|
|
||||||
const timelineTicks: Array<JSX.Element> = [];
|
const timelineTicks: Array<JSX.Element> = [];
|
||||||
for (let tickIndex = 0; tickIndex < tickCount; tickIndex += 1) {
|
for (
|
||||||
|
let tickIndex = startTickIndex;
|
||||||
|
tickIndex <= endTickIndex;
|
||||||
|
tickIndex += 1
|
||||||
|
) {
|
||||||
const time = tickIndex * tickIntervalSeconds;
|
const time = tickIndex * tickIntervalSeconds;
|
||||||
if (time > effectiveDuration) break;
|
if (time > effectiveDuration) break;
|
||||||
|
|
||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
TooltipContent,
|
TooltipContent,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { SkipBack, SplitSquareHorizontal } from "lucide-react";
|
import { SplitSquareHorizontal } from "lucide-react";
|
||||||
import {
|
import {
|
||||||
SplitButton,
|
SplitButton,
|
||||||
SplitButtonLeft,
|
SplitButtonLeft,
|
||||||
@@ -14,10 +14,9 @@ import {
|
|||||||
SplitButtonSeparator,
|
SplitButtonSeparator,
|
||||||
} from "@/components/ui/split-button";
|
} from "@/components/ui/split-button";
|
||||||
import { Slider } from "@/components/ui/slider";
|
import { Slider } from "@/components/ui/slider";
|
||||||
import { formatTimeCode } from "@/lib/time";
|
|
||||||
import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
|
import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
|
||||||
import { EditableTimecode } from "@/components/editable-timecode";
|
import { sliderToZoom, zoomToSlider } from "@/lib/timeline/zoom-utils";
|
||||||
import { ScenesView } from "../scenes-view";
|
import { ScenesView } from "../../scenes-view";
|
||||||
import { type TAction, invokeAction } from "@/lib/actions";
|
import { type TAction, invokeAction } from "@/lib/actions";
|
||||||
import { cn } from "@/utils/ui";
|
import { cn } from "@/utils/ui";
|
||||||
import { useTimelineStore } from "@/stores/timeline-store";
|
import { useTimelineStore } from "@/stores/timeline-store";
|
||||||
@@ -31,8 +30,6 @@ import {
|
|||||||
Link04Icon,
|
Link04Icon,
|
||||||
SearchAddIcon,
|
SearchAddIcon,
|
||||||
SearchMinusIcon,
|
SearchMinusIcon,
|
||||||
PauseIcon,
|
|
||||||
PlayIcon,
|
|
||||||
Copy01Icon,
|
Copy01Icon,
|
||||||
AlignLeftIcon,
|
AlignLeftIcon,
|
||||||
AlignRightIcon,
|
AlignRightIcon,
|
||||||
@@ -54,9 +51,9 @@ export function TimelineToolbar({
|
|||||||
direction === "in"
|
direction === "in"
|
||||||
? Math.min(
|
? Math.min(
|
||||||
TIMELINE_CONSTANTS.ZOOM_MAX,
|
TIMELINE_CONSTANTS.ZOOM_MAX,
|
||||||
zoomLevel + TIMELINE_CONSTANTS.ZOOM_STEP,
|
zoomLevel * TIMELINE_CONSTANTS.ZOOM_BUTTON_FACTOR,
|
||||||
)
|
)
|
||||||
: Math.max(minZoom, zoomLevel - TIMELINE_CONSTANTS.ZOOM_STEP);
|
: Math.max(minZoom, zoomLevel / TIMELINE_CONSTANTS.ZOOM_BUTTON_FACTOR);
|
||||||
setZoomLevel({ zoom: newZoomLevel });
|
setZoomLevel({ zoom: newZoomLevel });
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -81,7 +78,6 @@ export function TimelineToolbar({
|
|||||||
function ToolbarLeftSection() {
|
function ToolbarLeftSection() {
|
||||||
const editor = useEditor();
|
const editor = useEditor();
|
||||||
const currentTime = editor.playback.getCurrentTime();
|
const currentTime = editor.playback.getCurrentTime();
|
||||||
const isPlaying = editor.playback.getIsPlaying();
|
|
||||||
const currentBookmarked = editor.scenes.isBookmarked({ time: currentTime });
|
const currentBookmarked = editor.scenes.isBookmarked({ time: currentTime });
|
||||||
|
|
||||||
const handleAction = ({
|
const handleAction = ({
|
||||||
@@ -98,46 +94,16 @@ function ToolbarLeftSection() {
|
|||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<TooltipProvider delayDuration={500}>
|
<TooltipProvider delayDuration={500}>
|
||||||
<ToolbarButton
|
|
||||||
icon={
|
|
||||||
isPlaying ? (
|
|
||||||
<HugeiconsIcon icon={PauseIcon} />
|
|
||||||
) : (
|
|
||||||
<HugeiconsIcon icon={PlayIcon} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
tooltip={isPlaying ? "Pause" : "Play"}
|
|
||||||
onClick={({ event }) =>
|
|
||||||
handleAction({ action: "toggle-play", event })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ToolbarButton
|
|
||||||
icon={<SkipBack />}
|
|
||||||
tooltip="Go to start"
|
|
||||||
onClick={({ event }) => handleAction({ action: "goto-start", event })}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="bg-border mx-1 h-6 w-px" />
|
|
||||||
|
|
||||||
<TimeDisplay />
|
|
||||||
|
|
||||||
<div className="bg-border mx-1 h-6 w-px" />
|
|
||||||
|
|
||||||
<ToolbarButton
|
<ToolbarButton
|
||||||
icon={<HugeiconsIcon icon={ScissorIcon} />}
|
icon={<HugeiconsIcon icon={ScissorIcon} />}
|
||||||
tooltip="Split element"
|
tooltip="Split element"
|
||||||
onClick={({ event }) =>
|
onClick={({ event }) => handleAction({ action: "split", event })}
|
||||||
handleAction({ action: "split", event })
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ToolbarButton
|
<ToolbarButton
|
||||||
icon={<HugeiconsIcon icon={AlignLeftIcon} />}
|
icon={<HugeiconsIcon icon={AlignLeftIcon} />}
|
||||||
tooltip="Split left"
|
tooltip="Split left"
|
||||||
onClick={({ event }) =>
|
onClick={({ event }) => handleAction({ action: "split-left", event })}
|
||||||
handleAction({ action: "split-left", event })
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ToolbarButton
|
<ToolbarButton
|
||||||
@@ -182,12 +148,8 @@ function ToolbarLeftSection() {
|
|||||||
|
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<ToolbarButton
|
<ToolbarButton
|
||||||
icon={
|
icon={<HugeiconsIcon icon={Bookmark02Icon} />}
|
||||||
<HugeiconsIcon
|
isActive={currentBookmarked}
|
||||||
icon={Bookmark02Icon}
|
|
||||||
className={currentBookmarked ? "fill-primary text-primary" : ""}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
tooltip={currentBookmarked ? "Remove bookmark" : "Add bookmark"}
|
tooltip={currentBookmarked ? "Remove bookmark" : "Add bookmark"}
|
||||||
onClick={({ event }) =>
|
onClick={({ event }) =>
|
||||||
handleAction({ action: "toggle-bookmark", event })
|
handleAction({ action: "toggle-bookmark", event })
|
||||||
@@ -199,34 +161,6 @@ function ToolbarLeftSection() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function TimeDisplay() {
|
|
||||||
const editor = useEditor();
|
|
||||||
const currentTime = editor.playback.getCurrentTime();
|
|
||||||
const totalDuration = editor.timeline.getTotalDuration();
|
|
||||||
const fps = editor.project.getActive().settings.fps;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-row items-center justify-center px-2">
|
|
||||||
<EditableTimecode
|
|
||||||
time={currentTime}
|
|
||||||
duration={totalDuration}
|
|
||||||
format="HH:MM:SS:FF"
|
|
||||||
fps={fps}
|
|
||||||
onTimeChange={({ time }) => editor.playback.seek({ time })}
|
|
||||||
className="text-center"
|
|
||||||
/>
|
|
||||||
<div className="text-muted-foreground px-2 font-mono text-xs">/</div>
|
|
||||||
<div className="text-muted-foreground text-center font-mono text-xs">
|
|
||||||
{formatTimeCode({
|
|
||||||
timeInSeconds: totalDuration,
|
|
||||||
format: "HH:MM:SS:FF",
|
|
||||||
fps,
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function SceneSelector() {
|
function SceneSelector() {
|
||||||
const editor = useEditor();
|
const editor = useEditor();
|
||||||
const currentScene = editor.scenes.getActiveScene();
|
const currentScene = editor.scenes.getActiveScene();
|
||||||
@@ -237,7 +171,7 @@ function SceneSelector() {
|
|||||||
<SplitButtonLeft>{currentScene?.name || "No Scene"}</SplitButtonLeft>
|
<SplitButtonLeft>{currentScene?.name || "No Scene"}</SplitButtonLeft>
|
||||||
<SplitButtonSeparator />
|
<SplitButtonSeparator />
|
||||||
<ScenesView>
|
<ScenesView>
|
||||||
<SplitButtonRight onClick={() => {}} type="button">
|
<SplitButtonRight onClick={() => {}}>
|
||||||
<HugeiconsIcon icon={Layers01Icon} className="size-4" />
|
<HugeiconsIcon icon={Layers01Icon} className="size-4" />
|
||||||
</SplitButtonRight>
|
</SplitButtonRight>
|
||||||
</ScenesView>
|
</ScenesView>
|
||||||
@@ -268,26 +202,15 @@ function ToolbarRightSection({
|
|||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<TooltipProvider delayDuration={500}>
|
<TooltipProvider delayDuration={500}>
|
||||||
<ToolbarButton
|
<ToolbarButton
|
||||||
icon={
|
icon={<HugeiconsIcon icon={MagnetIcon} />}
|
||||||
<HugeiconsIcon
|
isActive={snappingEnabled}
|
||||||
icon={MagnetIcon}
|
|
||||||
className={cn(snappingEnabled ? "text-primary" : "")}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
tooltip="Auto snapping"
|
tooltip="Auto snapping"
|
||||||
onClick={() => toggleSnapping()}
|
onClick={() => toggleSnapping()}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ToolbarButton
|
<ToolbarButton
|
||||||
icon={
|
icon={<HugeiconsIcon icon={Link04Icon} className="scale-110" />}
|
||||||
<HugeiconsIcon
|
isActive={rippleEditingEnabled}
|
||||||
icon={Link04Icon}
|
|
||||||
className={cn(
|
|
||||||
rippleEditingEnabled ? "text-primary" : "",
|
|
||||||
"scale-110",
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
tooltip="Ripple editing"
|
tooltip="Ripple editing"
|
||||||
onClick={() => toggleRippleEditing()}
|
onClick={() => toggleRippleEditing()}
|
||||||
/>
|
/>
|
||||||
@@ -299,26 +222,26 @@ function ToolbarRightSection({
|
|||||||
<Button
|
<Button
|
||||||
variant="text"
|
variant="text"
|
||||||
size="icon"
|
size="icon"
|
||||||
type="button"
|
|
||||||
onClick={() => onZoom({ direction: "out" })}
|
onClick={() => onZoom({ direction: "out" })}
|
||||||
>
|
>
|
||||||
<HugeiconsIcon icon={SearchAddIcon} />
|
<HugeiconsIcon icon={SearchMinusIcon} />
|
||||||
</Button>
|
</Button>
|
||||||
<Slider
|
<Slider
|
||||||
className="w-24"
|
className="w-28"
|
||||||
value={[zoomLevel]}
|
value={[zoomToSlider({ zoomLevel, minZoom })]}
|
||||||
onValueChange={(values) => onZoomChange(values[0])}
|
onValueChange={(values) =>
|
||||||
min={minZoom}
|
onZoomChange(sliderToZoom({ sliderPosition: values[0], minZoom }))
|
||||||
max={TIMELINE_CONSTANTS.ZOOM_MAX}
|
}
|
||||||
step={TIMELINE_CONSTANTS.ZOOM_STEP}
|
min={0}
|
||||||
|
max={1}
|
||||||
|
step={0.005}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
variant="text"
|
variant="text"
|
||||||
size="icon"
|
size="icon"
|
||||||
type="button"
|
|
||||||
onClick={() => onZoom({ direction: "in" })}
|
onClick={() => onZoom({ direction: "in" })}
|
||||||
>
|
>
|
||||||
<HugeiconsIcon icon={SearchMinusIcon} />
|
<HugeiconsIcon icon={SearchAddIcon} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -330,21 +253,25 @@ function ToolbarButton({
|
|||||||
tooltip,
|
tooltip,
|
||||||
onClick,
|
onClick,
|
||||||
disabled,
|
disabled,
|
||||||
|
isActive,
|
||||||
}: {
|
}: {
|
||||||
icon: React.ReactNode;
|
icon: React.ReactNode;
|
||||||
tooltip: string;
|
tooltip: string;
|
||||||
onClick: ({ event }: { event: React.MouseEvent }) => void;
|
onClick: ({ event }: { event: React.MouseEvent }) => void;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
isActive?: boolean;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Tooltip delayDuration={200}>
|
<Tooltip delayDuration={200}>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="text"
|
variant={isActive ? "secondary" : "text"}
|
||||||
size="icon"
|
size="icon"
|
||||||
type="button"
|
|
||||||
onClick={(event) => onClick({ event })}
|
onClick={(event) => onClick({ event })}
|
||||||
className={disabled ? "cursor-not-allowed opacity-50" : ""}
|
className={cn(
|
||||||
|
"rounded-sm",
|
||||||
|
disabled ? "cursor-not-allowed opacity-50" : "",
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{icon}
|
{icon}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -9,7 +9,6 @@ import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
|
|||||||
import { useEdgeAutoScroll } from "@/hooks/timeline/use-edge-auto-scroll";
|
import { useEdgeAutoScroll } from "@/hooks/timeline/use-edge-auto-scroll";
|
||||||
import type { ElementDragState } from "@/types/timeline";
|
import type { ElementDragState } from "@/types/timeline";
|
||||||
import { useEditor } from "@/hooks/use-editor";
|
import { useEditor } from "@/hooks/use-editor";
|
||||||
import { cn } from "@/utils/ui";
|
|
||||||
|
|
||||||
interface TimelineTrackContentProps {
|
interface TimelineTrackContentProps {
|
||||||
track: TimelineTrack;
|
track: TimelineTrack;
|
||||||
@@ -31,6 +30,7 @@ interface TimelineTrackContentProps {
|
|||||||
track: TimelineTrack;
|
track: TimelineTrack;
|
||||||
}) => void;
|
}) => void;
|
||||||
onTrackMouseDown?: (event: React.MouseEvent) => void;
|
onTrackMouseDown?: (event: React.MouseEvent) => void;
|
||||||
|
onTrackClick?: (event: React.MouseEvent) => void;
|
||||||
shouldIgnoreClick?: () => boolean;
|
shouldIgnoreClick?: () => boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,6 +46,7 @@ export function TimelineTrackContent({
|
|||||||
onElementMouseDown,
|
onElementMouseDown,
|
||||||
onElementClick,
|
onElementClick,
|
||||||
onTrackMouseDown,
|
onTrackMouseDown,
|
||||||
|
onTrackClick,
|
||||||
shouldIgnoreClick,
|
shouldIgnoreClick,
|
||||||
}: TimelineTrackContentProps) {
|
}: TimelineTrackContentProps) {
|
||||||
const editor = useEditor();
|
const editor = useEditor();
|
||||||
@@ -61,18 +62,16 @@ export function TimelineTrackContent({
|
|||||||
contentWidth: duration * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel,
|
contentWidth: duration * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel,
|
||||||
});
|
});
|
||||||
|
|
||||||
const hasSelectedElements = track.elements.some((element) =>
|
|
||||||
isElementSelected({ trackId: track.id, elementId: element.id }),
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className={cn("size-full", hasSelectedElements && "bg-panel-accent/35")}
|
className="size-full"
|
||||||
onClick={() => {
|
onClick={(event) => {
|
||||||
if (shouldIgnoreClick?.()) return;
|
if (shouldIgnoreClick?.()) return;
|
||||||
clearElementSelection();
|
clearElementSelection();
|
||||||
|
onTrackClick?.(event);
|
||||||
}}
|
}}
|
||||||
onMouseDown={(event) => {
|
onMouseDown={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
onTrackMouseDown?.(event);
|
onTrackMouseDown?.(event);
|
||||||
}}
|
}}
|
||||||
type="button"
|
type="button"
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
import { useEditor } from "@/hooks/use-editor";
|
|
||||||
import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
|
|
||||||
import { getSnappedSeekTime } from "@/lib/time";
|
|
||||||
import { Bookmark02Icon } from "@hugeicons/core-free-icons";
|
|
||||||
import { HugeiconsIcon } from "@hugeicons/react";
|
|
||||||
|
|
||||||
interface TimelineBookmarksRowProps {
|
|
||||||
zoomLevel: number;
|
|
||||||
dynamicTimelineWidth: number;
|
|
||||||
handleWheel: (e: React.WheelEvent) => void;
|
|
||||||
handleTimelineContentClick: (e: React.MouseEvent) => void;
|
|
||||||
handleRulerTrackingMouseDown: (e: React.MouseEvent) => void;
|
|
||||||
handleRulerMouseDown: (e: React.MouseEvent) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function TimelineBookmarksRow({
|
|
||||||
zoomLevel,
|
|
||||||
dynamicTimelineWidth,
|
|
||||||
handleWheel,
|
|
||||||
handleTimelineContentClick,
|
|
||||||
handleRulerTrackingMouseDown,
|
|
||||||
handleRulerMouseDown,
|
|
||||||
}: TimelineBookmarksRowProps) {
|
|
||||||
const editor = useEditor();
|
|
||||||
const activeScene = editor.scenes.getActiveScene();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="relative h-4 flex-1 overflow-hidden">
|
|
||||||
<button
|
|
||||||
className="relative h-4 w-full cursor-default select-none border-0 bg-transparent p-0"
|
|
||||||
style={{
|
|
||||||
width: `${dynamicTimelineWidth}px`,
|
|
||||||
}}
|
|
||||||
aria-label="Timeline ruler"
|
|
||||||
type="button"
|
|
||||||
onWheel={handleWheel}
|
|
||||||
onClick={handleTimelineContentClick}
|
|
||||||
onMouseDown={(event) => {
|
|
||||||
handleRulerMouseDown(event);
|
|
||||||
handleRulerTrackingMouseDown(event);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{activeScene.bookmarks.map((time: number) => (
|
|
||||||
<TimelineBookmark
|
|
||||||
key={`bookmark-row-${time}`}
|
|
||||||
time={time}
|
|
||||||
zoomLevel={zoomLevel}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function TimelineBookmark({
|
|
||||||
time,
|
|
||||||
zoomLevel,
|
|
||||||
}: {
|
|
||||||
time: number;
|
|
||||||
zoomLevel: number;
|
|
||||||
}) {
|
|
||||||
const editor = useEditor();
|
|
||||||
const activeProject = editor.project.getActive();
|
|
||||||
const duration = editor.timeline.getTotalDuration();
|
|
||||||
|
|
||||||
const handleBookmarkActivate = ({
|
|
||||||
event,
|
|
||||||
}: {
|
|
||||||
event:
|
|
||||||
| React.MouseEvent<HTMLButtonElement>
|
|
||||||
| React.KeyboardEvent<HTMLButtonElement>;
|
|
||||||
}) => {
|
|
||||||
event.stopPropagation();
|
|
||||||
const framesPerSecond = activeProject?.settings.fps ?? 30;
|
|
||||||
const snappedTime = getSnappedSeekTime({
|
|
||||||
rawTime: time,
|
|
||||||
duration,
|
|
||||||
fps: framesPerSecond,
|
|
||||||
});
|
|
||||||
editor.playback.seek({ time: snappedTime });
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
className="absolute top-0 h-10 w-0.5 cursor-pointer border-0 bg-transparent p-0"
|
|
||||||
style={{
|
|
||||||
left: `${time * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel}px`,
|
|
||||||
}}
|
|
||||||
aria-label={`Seek to bookmark at ${time}s`}
|
|
||||||
type="button"
|
|
||||||
onMouseDown={(event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
}}
|
|
||||||
onClick={(event) => handleBookmarkActivate({ event })}
|
|
||||||
onKeyDown={(event) => {
|
|
||||||
if (event.key !== "Enter" && event.key !== " ") return;
|
|
||||||
event.preventDefault();
|
|
||||||
handleBookmarkActivate({ event });
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="text-primary absolute top-[-1px] left-[-5px]">
|
|
||||||
<HugeiconsIcon
|
|
||||||
icon={Bookmark02Icon}
|
|
||||||
aria-hidden="true"
|
|
||||||
className="fill-primary size-3"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useFilmstrip } from "@/hooks/timeline/use-filmstrip";
|
|
||||||
import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
|
|
||||||
import { getThumbnailWidth } from "@/lib/timeline/filmstrip-utils";
|
|
||||||
|
|
||||||
interface FilmstripProps {
|
|
||||||
mediaId: string;
|
|
||||||
file: File | null;
|
|
||||||
duration: number;
|
|
||||||
trimStart: number;
|
|
||||||
trackHeight: number;
|
|
||||||
zoomLevel: number;
|
|
||||||
fallbackThumbnailUrl?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Filmstrip({
|
|
||||||
mediaId,
|
|
||||||
file,
|
|
||||||
duration,
|
|
||||||
trimStart,
|
|
||||||
trackHeight,
|
|
||||||
zoomLevel,
|
|
||||||
fallbackThumbnailUrl,
|
|
||||||
}: FilmstripProps) {
|
|
||||||
const thumbnailWidth = getThumbnailWidth({ trackHeight });
|
|
||||||
const pixelsPerSecond = TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel;
|
|
||||||
|
|
||||||
const visibleStartTime = trimStart;
|
|
||||||
const visibleEndTime = trimStart + duration;
|
|
||||||
|
|
||||||
const { frames } = useFilmstrip({
|
|
||||||
mediaId,
|
|
||||||
file,
|
|
||||||
duration,
|
|
||||||
visibleStartTime,
|
|
||||||
visibleEndTime,
|
|
||||||
zoomLevel,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (frames.length === 0 && fallbackThumbnailUrl) {
|
|
||||||
return (
|
|
||||||
<div className="absolute inset-0 flex items-center justify-center">
|
|
||||||
<div
|
|
||||||
className="absolute right-0 left-0"
|
|
||||||
style={{
|
|
||||||
backgroundImage: `url(${fallbackThumbnailUrl})`,
|
|
||||||
backgroundRepeat: "repeat-x",
|
|
||||||
backgroundSize: `${thumbnailWidth}px ${trackHeight}px`,
|
|
||||||
backgroundPosition: "left center",
|
|
||||||
pointerEvents: "none",
|
|
||||||
top: 0,
|
|
||||||
bottom: 0,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="absolute inset-0 overflow-hidden">
|
|
||||||
{frames.map((frame) => {
|
|
||||||
const left = (frame.timestamp - visibleStartTime) * pixelsPerSecond;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={`${frame.mediaId}-${frame.timestamp}-${frame.tier}`}
|
|
||||||
className="absolute top-0 h-full bg-cover bg-center"
|
|
||||||
style={{
|
|
||||||
left: `${left}px`,
|
|
||||||
width: `${thumbnailWidth}px`,
|
|
||||||
backgroundImage: `url(${frame.dataUrl})`,
|
|
||||||
pointerEvents: "none",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,7 @@
|
|||||||
import { Button } from "./ui/button";
|
import { Button } from "./ui/button";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { SOCIAL_LINKS } from "@/constants/site-constants";
|
import { SOCIAL_LINKS } from "@/constants/site-constants";
|
||||||
import {
|
import { GithubIcon, Link04Icon } from "@hugeicons/core-free-icons";
|
||||||
GithubIcon,
|
|
||||||
Link04Icon,
|
|
||||||
} from "@hugeicons/core-free-icons";
|
|
||||||
import { HugeiconsIcon } from "@hugeicons/react";
|
import { HugeiconsIcon } from "@hugeicons/react";
|
||||||
|
|
||||||
export function GitHubContributeSection({
|
export function GitHubContributeSection({
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
useKeybindingDisabler,
|
useKeybindingDisabler,
|
||||||
} from "@/hooks/use-keybindings";
|
} from "@/hooks/use-keybindings";
|
||||||
import { useEditorActions } from "@/hooks/actions/use-editor-actions";
|
import { useEditorActions } from "@/hooks/actions/use-editor-actions";
|
||||||
|
import { prefetchFontAtlas } from "@/lib/fonts/google-fonts";
|
||||||
|
|
||||||
interface EditorProviderProps {
|
interface EditorProviderProps {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
@@ -42,6 +43,7 @@ export function EditorProvider({ projectId, children }: EditorProviderProps) {
|
|||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
|
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
|
prefetchFontAtlas();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
|
|
||||||
|
|||||||
@@ -22,14 +22,17 @@ export function ThemeToggle({
|
|||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
size="icon"
|
size="icon"
|
||||||
variant="text"
|
variant="ghost"
|
||||||
className={cn("h-7", className)}
|
className={cn("size-8", className)}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
setTheme(theme === "dark" ? "light" : "dark");
|
setTheme(theme === "dark" ? "light" : "dark");
|
||||||
onToggle?.(e);
|
onToggle?.(e);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<HugeiconsIcon icon={Sun03Icon} className={cn("!size-[1.1rem]", iconClassName)} />
|
<HugeiconsIcon
|
||||||
|
icon={Sun03Icon}
|
||||||
|
className={cn("!size-[1.1rem]", iconClassName)}
|
||||||
|
/>
|
||||||
<span className="sr-only">{theme === "dark" ? "Light" : "Dark"}</span>
|
<span className="sr-only">{theme === "dark" ? "Light" : "Dark"}</span>
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { cva, type VariantProps } from "class-variance-authority";
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
import { AlertTriangle } from "lucide-react";
|
|
||||||
import { cn } from "@/utils/ui";
|
import { cn } from "@/utils/ui";
|
||||||
import { HugeiconsIcon } from "@hugeicons/react";
|
import { HugeiconsIcon } from "@hugeicons/react";
|
||||||
import { Alert02Icon } from "@hugeicons/core-free-icons";
|
import { Alert02Icon } from "@hugeicons/core-free-icons";
|
||||||
@@ -32,7 +31,10 @@ const Alert = React.forwardRef<
|
|||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{variant === "destructive" && (
|
{variant === "destructive" && (
|
||||||
<HugeiconsIcon icon={Alert02Icon} className="size-5 text-destructive mt-0.5" />
|
<HugeiconsIcon
|
||||||
|
icon={Alert02Icon}
|
||||||
|
className="size-5 text-destructive mt-0.5"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
@@ -45,7 +47,10 @@ const AlertTitle = React.forwardRef<
|
|||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<h5
|
<h5
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn("mb-2 text-base leading-none font-semibold tracking-tight", className)}
|
className={cn(
|
||||||
|
"mb-2 text-base leading-none font-semibold tracking-tight",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
|
|||||||
@@ -38,10 +38,10 @@ const AvatarFallback = React.forwardRef<
|
|||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<AvatarPrimitive.Fallback
|
<AvatarPrimitive.Fallback
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-muted flex size-full items-center justify-center rounded-full",
|
"bg-muted flex size-full items-center justify-center rounded-full",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
|
|||||||
@@ -1,108 +1,99 @@
|
|||||||
import * as React from "react";
|
|
||||||
import { Slot as SlotPrimitive } from "radix-ui";
|
|
||||||
import { ChevronRight, MoreHorizontal } from "lucide-react";
|
import { ChevronRight, MoreHorizontal } from "lucide-react";
|
||||||
|
import { Slot } from "radix-ui";
|
||||||
|
|
||||||
import { cn } from "@/utils/ui";
|
import { cn } from "@/utils/ui";
|
||||||
|
|
||||||
const Breadcrumb = React.forwardRef<
|
function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
|
||||||
HTMLElement,
|
return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />;
|
||||||
React.ComponentPropsWithoutRef<"nav"> & {
|
}
|
||||||
separator?: React.ReactNode;
|
|
||||||
}
|
|
||||||
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />);
|
|
||||||
Breadcrumb.displayName = "Breadcrumb";
|
|
||||||
|
|
||||||
const BreadcrumbList = React.forwardRef<
|
|
||||||
HTMLOListElement,
|
|
||||||
React.ComponentPropsWithoutRef<"ol">
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<ol
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
BreadcrumbList.displayName = "BreadcrumbList";
|
|
||||||
|
|
||||||
const BreadcrumbItem = React.forwardRef<
|
|
||||||
HTMLLIElement,
|
|
||||||
React.ComponentPropsWithoutRef<"li">
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<li
|
|
||||||
ref={ref}
|
|
||||||
className={cn("inline-flex items-center gap-1.5", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
BreadcrumbItem.displayName = "BreadcrumbItem";
|
|
||||||
|
|
||||||
const BreadcrumbLink = React.forwardRef<
|
|
||||||
HTMLAnchorElement,
|
|
||||||
React.ComponentPropsWithoutRef<"a"> & {
|
|
||||||
asChild?: boolean;
|
|
||||||
}
|
|
||||||
>(({ asChild, className, ...props }, ref) => {
|
|
||||||
const Comp = asChild ? SlotPrimitive.Slot : "a";
|
|
||||||
|
|
||||||
|
function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
|
||||||
return (
|
return (
|
||||||
<Comp
|
<ol
|
||||||
ref={ref}
|
data-slot="breadcrumb-list"
|
||||||
className={cn("hover:text-foreground", className)}
|
className={cn(
|
||||||
|
"text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
BreadcrumbLink.displayName = "BreadcrumbLink";
|
|
||||||
|
|
||||||
const BreadcrumbPage = React.forwardRef<
|
function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
|
||||||
HTMLSpanElement,
|
return (
|
||||||
React.ComponentPropsWithoutRef<"span">
|
<li
|
||||||
>(({ className, ...props }, ref) => (
|
data-slot="breadcrumb-item"
|
||||||
<span
|
className={cn("inline-flex items-center gap-1.5", className)}
|
||||||
ref={ref}
|
{...props}
|
||||||
role="link"
|
/>
|
||||||
aria-disabled="true"
|
);
|
||||||
aria-current="page"
|
}
|
||||||
className={cn("text-foreground font-normal", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
BreadcrumbPage.displayName = "BreadcrumbPage";
|
|
||||||
|
|
||||||
const BreadcrumbSeparator = ({
|
function BreadcrumbLink({
|
||||||
|
asChild,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"a"> & {
|
||||||
|
asChild?: boolean;
|
||||||
|
}) {
|
||||||
|
const Comp = asChild ? Slot.Root : "a";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
data-slot="breadcrumb-link"
|
||||||
|
className={cn("hover:text-foreground transition-colors", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
data-slot="breadcrumb-page"
|
||||||
|
aria-current="page"
|
||||||
|
className={cn("text-foreground font-normal", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function BreadcrumbSeparator({
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"li">) => (
|
}: React.ComponentProps<"li">) {
|
||||||
<li
|
return (
|
||||||
role="presentation"
|
<li
|
||||||
aria-hidden="true"
|
data-slot="breadcrumb-separator"
|
||||||
className={cn("[&>svg]:size-3.5", className)}
|
role="presentation"
|
||||||
{...props}
|
aria-hidden="true"
|
||||||
>
|
className={cn("[&>svg]:size-3.5", className)}
|
||||||
{children ?? <ChevronRight />}
|
{...props}
|
||||||
</li>
|
>
|
||||||
);
|
{children ?? <ChevronRight />}
|
||||||
BreadcrumbSeparator.displayName = "BreadcrumbSeparator";
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const BreadcrumbEllipsis = ({
|
function BreadcrumbEllipsis({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"span">) => (
|
}: React.ComponentProps<"span">) {
|
||||||
<span
|
return (
|
||||||
role="presentation"
|
<span
|
||||||
aria-hidden="true"
|
data-slot="breadcrumb-ellipsis"
|
||||||
className={cn("flex size-9 items-center justify-center", className)}
|
role="presentation"
|
||||||
{...props}
|
aria-hidden="true"
|
||||||
>
|
className={cn("flex size-9 items-center justify-center", className)}
|
||||||
<MoreHorizontal className="size-4" />
|
{...props}
|
||||||
<span className="sr-only">More</span>
|
>
|
||||||
</span>
|
<MoreHorizontal className="size-4" />
|
||||||
);
|
<span className="sr-only">More</span>
|
||||||
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis";
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Breadcrumb,
|
Breadcrumb,
|
||||||
|
|||||||