diff --git a/src/routes/projects/[projectId]/+layout.svelte b/src/routes/projects/[projectId]/+layout.svelte index f00181a62..a4ec85ea1 100644 --- a/src/routes/projects/[projectId]/+layout.svelte +++ b/src/routes/projects/[projectId]/+layout.svelte @@ -32,96 +32,98 @@
- +
+ + + + {/if}
diff --git a/src/routes/projects/[projectId]/player/+page.svelte b/src/routes/projects/[projectId]/player/+page.svelte index 92959a205..e86c07683 100644 --- a/src/routes/projects/[projectId]/player/+page.svelte +++ b/src/routes/projects/[projectId]/player/+page.svelte @@ -12,150 +12,205 @@ const { sessions } = data; let currentTimestamp = new Date().getTime(); + let currentPlayerValue = 0; + let currentDay = dateToYmd(new Date()); - $: minVisibleTimestamp = - $sessions.length > 0 - ? Math.max( - Math.min(currentTimestamp - 12 * 60 * 60 * 1000, $sessions[0].meta.startTimestampMs), - $sessions.at(-1)!.meta.startTimestampMs - ) - : 0; + $: sessionDays = $sessions.reduce((group, session) => { + const day = dateToYmd(new Date(session.meta.startTimestampMs)); + group[day] = group[day] ?? []; + group[day].push(session); + // sort by startTimestampMs + group[day].sort((a, b) => a.meta.startTimestampMs - b.meta.startTimestampMs); + return group; + }, {}); - let maxVisibleTimestamp = new Date().getTime(); - onMount(() => { - const inverval = setInterval(() => { - maxVisibleTimestamp = new Date().getTime(); - }, 1000); - return () => clearInterval(inverval); + $: currentSessions = $sessions.filter((session) => { + let sessionDay = dateToYmd(new Date(session.meta.startTimestampMs)); + return sessionDay === currentDay; }); - $: visibleSessions = $sessions.filter( - (session) => - session.meta.startTimestampMs >= minVisibleTimestamp || - session.meta.lastTimestampMs >= minVisibleTimestamp - ); - $: earliestVisibleSession = visibleSessions.at(-1)!; - - let deltasBySessionId: Record>> = {}; - $: visibleSessions - .filter((s) => deltasBySessionId[s.id] === undefined) + let currentDeltas: Record>> = {}; + $: currentSessions + .filter((s) => currentDeltas[s.id] === undefined) .forEach((s) => { - deltasBySessionId[s.id] = listDeltas({ + currentDeltas[s.id] = listDeltas({ projectId: data.projectId, sessionId: s.id }); }); - let docsBySessionId: Record>> = {}; - $: if (earliestVisibleSession && docsBySessionId[earliestVisibleSession.id] === undefined) { - docsBySessionId[earliestVisibleSession.id] = listFiles({ + type VideoFileEdit = { + filepath: string; + delta: Delta; + }; + + type VideoChapter = { + title: string; + session: string; + files: Record; + edits: VideoFileEdit[]; + editCount: number; + firstDeltaTimestampMs: number; + lastDeltaTimestampMs: number; + totalDurationMs: number; + }; + + type DayVideo = { + chapters: VideoChapter[]; + editCount: number; + totalDurationMs: number; + firstDeltaTimestampMs: number; + lastDeltaTimestampMs: number; + }; + + let dayPlaylist: Record = {}; + let sessionFiles: Record> = {}; + let sessionChapters: Record = {}; + + $: currentSessions.forEach((s) => listSession(s.id)); + + function listSession(sid: string) { + console.log('session', sid); + + currentDeltas[sid].then((deltas) => { + if (sessionChapters[sid] === undefined) { + sessionChapters[sid] = { + title: sid, + session: sid, + files: {}, + edits: [], + editCount: 0, + firstDeltaTimestampMs: 9999999999999, + lastDeltaTimestampMs: 0, + totalDurationMs: 0 + }; + } + sessionChapters[sid].edits = []; + + Object.entries(deltas).forEach(([filepath, deltas]) => { + deltas.forEach((delta) => { + sessionChapters[sid].edits.push({ + filepath, + delta + }); + }); + if (sessionFiles[sid] === undefined) sessionFiles[sid] = {}; + sessionFiles[sid][filepath] = ''; + sessionChapters[sid].editCount = sessionChapters[sid].edits.length; + sessionChapters[sid].files[filepath] = deltas.length; + sessionChapters[sid].firstDeltaTimestampMs = min( + deltas.at(0)!.timestampMs, + sessionChapters[sid].firstDeltaTimestampMs + ); + sessionChapters[sid].lastDeltaTimestampMs = max( + deltas.at(-1)!.timestampMs, + sessionChapters[sid].lastDeltaTimestampMs + ); + sessionChapters[sid].totalDurationMs = + sessionChapters[sid].lastDeltaTimestampMs - sessionChapters[sid].firstDeltaTimestampMs; + }); + + console.log('sessionChapters', sessionChapters[sid]); + // get the session chapters that are in the current day + let dayChapters = Object.entries(sessionChapters) + .filter(([sid, chapter]) => { + let chapterDay = dateToYmd(new Date(chapter.firstDeltaTimestampMs)); + return chapterDay === currentDay; + }) + .map(([, chapter]) => chapter) + .sort((a, b) => a.firstDeltaTimestampMs - b.firstDeltaTimestampMs); + + dayPlaylist[currentDay] = { + chapters: dayChapters, + editCount: dayChapters.reduce((acc, chapter) => acc + chapter.editCount, 0), + totalDurationMs: Object.values(dayChapters).reduce( + (acc, chapter) => acc + chapter.totalDurationMs, + 0 + ), + firstDeltaTimestampMs: Object.values(dayChapters).reduce( + (acc, chapter) => min(acc, chapter.firstDeltaTimestampMs), + 9999999999999 + ), + lastDeltaTimestampMs: Object.values(dayChapters).reduce( + (acc, chapter) => max(acc, chapter.lastDeltaTimestampMs), + 0 + ) + }; + console.log('dayPlaylist', dayPlaylist); + }); + + listFiles({ projectId: data.projectId, - sessionId: earliestVisibleSession.id + sessionId: sid + }).then((files) => { + Object.entries(sessionFiles[sid]).forEach(([filepath, _]) => { + if (files[filepath] === undefined) { + console.log('file not found', filepath); + } else { + sessionFiles[sid][filepath] = files[filepath]; + console.log('file found', sid, filepath, files[filepath]); + } + }); }); } - $: visibleDeltasByFilepath = Promise.all( - visibleSessions.map((s) => deltasBySessionId[s.id]) - ).then((deltasBySessionId) => - Object.values(deltasBySessionId).reduce((acc, deltasByFilepath) => { - Object.entries(deltasByFilepath).forEach(([filepath, deltas]) => { - deltas = deltas.filter((delta) => delta.timestampMs <= currentTimestamp); - if (acc[filepath] === undefined) acc[filepath] = []; - acc[filepath].push(...deltas); - }); - return acc; - }, {} as Record) - ); + function max(a: T, b: T): T { + return a > b ? a : b; + } - let frame: { + function min(a: T, b: T): T { + return a < b ? a : b; + } + + function dateToYmd(date: Date): string { + const year = date.getFullYear(); + const month = ('0' + (date.getMonth() + 1)).slice(-2); + const day = ('0' + date.getDate()).slice(-2); + return `${year}-${month}-${day}`; + } + + function ymdToDate(dateString: string): Date { + const [year, month, day] = dateString.split('-').map(Number); + return new Date(year, month - 1, day); + } + + function selectDay(day: string) { + return () => { + console.log('select day', day); + currentDay = day; + currentPlayerValue = 0; + }; + } + + type EditFrame = { + sessionId: string; + timestampMs: number; filepath: string; doc: string; - deltas: Delta[]; - } | null = null; - $: visibleDeltasByFilepath - .then( - (visibleDeltasByFilepath) => - Object.entries(visibleDeltasByFilepath) - .filter(([_, deltas]) => deltas.length > 0) - .map(([filepath, deltas]) => [filepath, deltas.at(-1)!.timestampMs] as [string, number]) - .sort((a, b) => b[1] - a[1]) - .at(0)?.[0] ?? null - ) - .then(async (visibleFilepath) => { - if (earliestVisibleSession && visibleFilepath !== null) { - frame = { - deltas: - (await visibleDeltasByFilepath.then((deltasByFilepath) => - deltasByFilepath[visibleFilepath].sort((a, b) => a.timestampMs - b.timestampMs) - )) || [], - doc: - (await docsBySessionId[earliestVisibleSession.id].then( - (docsByFilepath) => docsByFilepath[visibleFilepath] - )) || '', - filepath: visibleFilepath + ops: Delta[]; + delta: Delta; + }; + + let currentEdit: EditFrame | null = null; + $: if (currentPlayerValue > 0) { + let totalEdits = 0; + currentEdit = null; + dayPlaylist[currentDay].chapters.forEach((chapter) => { + console.log(totalEdits); + if (currentEdit == null && currentPlayerValue < totalEdits + chapter.editCount) { + let thisEdit = chapter.edits[currentPlayerValue - totalEdits]; + currentEdit = { + sessionId: chapter.session, + timestampMs: thisEdit.delta.timestampMs, + filepath: thisEdit.filepath, + doc: sessionFiles[chapter.session][thisEdit.filepath], + ops: [], + delta: thisEdit.delta }; } + totalEdits += chapter.editCount; }); - - // player - let interval: ReturnType | undefined; - let direction: -1 | 1 = 1; - let speed = 1; - let oneSecond = 1000; - - const stop = () => { - clearInterval(interval); - interval = undefined; - speed = 1; - }; - const play = () => start({ direction, speed }); - - const start = (params: { direction: 1 | -1; speed: number }) => { - if (interval) clearInterval(interval); - interval = setInterval(() => { - currentTimestamp += oneSecond * params.direction; - }, oneSecond / params.speed); - }; - - const speedUp = () => { - speed = speed * 2; - start({ direction, speed }); - }; - - $: visibleRanges = visibleSessions - .map( - ({ meta }) => - [ - Math.max(meta.startTimestampMs, minVisibleTimestamp), - Math.min(meta.lastTimestampMs, maxVisibleTimestamp) - ] as [number, number] - ) - .sort((a, b) => a[0] - b[0]) - .reduce((timeline, range) => { - const [from, to] = range; - const last = timeline.at(-1); - if (last) timeline.push([last[1], from, false]); - timeline.push([from, to, true]); - return timeline; - }, [] as [number, number, boolean][]); - - const rangeWidth = (range: [number, number]) => - (100 * (range[1] - range[0])) / (maxVisibleTimestamp - minVisibleTimestamp) + '%'; - - const timestampToOffset = (timestamp: number) => - ((timestamp - minVisibleTimestamp) / (maxVisibleTimestamp - minVisibleTimestamp)) * 100 + '%'; - - const offsetToTimestamp = (offset: number) => - offset * (maxVisibleTimestamp - minVisibleTimestamp) + minVisibleTimestamp; - - let timeline: HTMLElement; - - const onSelectTimestamp = (e: MouseEvent) => { - const { left, width } = timeline.getBoundingClientRect(); - const clickOffset = e.clientX; - const clickPos = Math.min(Math.max((clickOffset - left) / width, 0), 1) || 0; - currentTimestamp = offsetToTimestamp(clickPos); - }; + } {#if $sessions.length === 0} @@ -166,74 +221,77 @@
{:else} -
- {#if frame !== null} -
-

{frame.filepath}

-
- -
- -
- {/if} - -
-
(currentTimestamp = offsetToTimestamp(v))} - class="absolute flex h-12 w-4 cursor-pointer items-center justify-around transition hover:scale-150" - style:left="calc({timestampToOffset(currentTimestamp)} - 0.5rem)" - > -
-
- -
-
- {new Date(minVisibleTimestamp).toLocaleString()} +
+
+
Playlist
+ {#each Object.entries(sessionDays) as [day, sessions]} +
+
{day}
+ {sessions.length}
- -
- {new Date(maxVisibleTimestamp).toLocaleString()} + {/each} +
+
+
+
+ {ymdToDate(currentDay)} + {#if dayPlaylist[currentDay] !== undefined} +
{dayPlaylist[currentDay].chapters.length} chapters
+
{dayPlaylist[currentDay].editCount} edits
+
{Math.round(dayPlaylist[currentDay].totalDurationMs / 1000 / 60)} min
+ {#if currentEdit !== null} +
{currentEdit.sessionId}
+
{currentEdit.filepath}
+
{currentEdit.delta.timestampMs}
+
{new Date(currentEdit.delta.timestampMs)}
+
{currentEdit.delta.operations}
+ + {/if} + {:else} + loading... + {/if} +
{currentPlayerValue}
-
- -
-
-
- {#each visibleRanges as [from, to, filled]} -
- {/each} -
+
Meta Data
+
+
play
+
+ {#if dayPlaylist[currentDay] !== undefined} + + {/if} +
+
2x
- -
- {#if interval} - - {:else} - - {/if} - +
+
Sessions
+
+ {#each sessionDays[currentDay] as session} +
+ {new Date(session.meta.startTimestampMs).toLocaleTimeString()} + to + {new Date(session.meta.lastTimestampMs).toLocaleTimeString()} + {#if sessionChapters[session.id] !== undefined} +
{Math.round(sessionChapters[session.id].totalDurationMs / 1000 / 60)} min
+ {#each Object.entries(sessionChapters[session.id].files) as [filepath, count]} +
{filepath}
+
{count}
+ {/each} + {/if} +
+ {/each} +
{/if}