mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-11-28 04:47:42 +03:00
new player
This commit is contained in:
parent
a8a2e2b0a9
commit
c024510a69
@ -32,96 +32,98 @@
|
||||
</script>
|
||||
|
||||
<div class="flex h-full w-full flex-col">
|
||||
<nav
|
||||
class="project-top-bar flex flex-none select-none items-center justify-between space-x-3 border-b border-zinc-700 p-[6px] px-8 text-zinc-300"
|
||||
>
|
||||
<div class="flex flex-row items-center space-x-2">
|
||||
<form action="/projects/{$project?.id}/search" method="GET">
|
||||
<label
|
||||
for="default-search"
|
||||
class="sr-only mb-2 text-sm font-medium text-gray-900 dark:text-white">Search</label
|
||||
>
|
||||
<div class="relative">
|
||||
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
||||
<svg class="mr-2 h-5 w-5" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"
|
||||
><path
|
||||
d="M8 12a4 4 0 110-8 4 4 0 010 8zm9.707 4.293l-4.82-4.82A5.968 5.968 0 0014 8 6 6 0 002 8a6 6 0 006 6 5.968 5.968 0 003.473-1.113l4.82 4.82a.997.997 0 001.414 0 .999.999 0 000-1.414z"
|
||||
fill="#5C5F62"
|
||||
/></svg
|
||||
>
|
||||
</div>
|
||||
<div class="flex w-48 max-w-lg rounded-md shadow-sm">
|
||||
<input
|
||||
type="text"
|
||||
name="search"
|
||||
id="search"
|
||||
placeholder="search"
|
||||
autocomplete="off"
|
||||
aria-label="Search input"
|
||||
class="block w-full min-w-0 flex-1 rounded border border-zinc-700 bg-zinc-800 p-[3px] px-2 pl-10 text-zinc-200 placeholder:text-zinc-500 sm:text-sm sm:leading-6"
|
||||
style=""
|
||||
/>
|
||||
<div
|
||||
class="absolute right-1 top-1 inline-flex items-center rounded border border-zinc-700/20 bg-zinc-700/50 px-1 py-[2px] text-gray-400 shadow sm:text-sm"
|
||||
>
|
||||
⌘K
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<a href="/projects/{$project?.id}/player" class="text-zinc-400 hover:text-zinc-200">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="h-6 w-6"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M15.91 11.672a.375.375 0 010 .656l-5.603 3.113a.375.375 0 01-.557-.328V8.887c0-.286.307-.466.557-.327l5.603 3.112z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<a href="/projects/{$project?.id}/settings" class="text-zinc-400 hover:text-zinc-300">
|
||||
<div class="rounded-md p-1 hover:bg-zinc-700 hover:bg-zinc-700 hover:text-zinc-200">
|
||||
<div class="h-6 w-6 ">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="h-6 w-6"
|
||||
{#if selection != 'player'}
|
||||
<nav
|
||||
class="project-top-bar flex flex-none select-none items-center justify-between space-x-3 border-b border-zinc-700 p-[6px] px-8 text-zinc-300"
|
||||
>
|
||||
<div class="flex flex-row items-center space-x-2">
|
||||
<form action="/projects/{$project?.id}/search" method="GET">
|
||||
<label
|
||||
for="default-search"
|
||||
class="sr-only mb-2 text-sm font-medium text-gray-900 dark:text-white">Search</label
|
||||
>
|
||||
<div class="relative">
|
||||
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
||||
<svg class="mr-2 h-5 w-5" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"
|
||||
><path
|
||||
d="M8 12a4 4 0 110-8 4 4 0 010 8zm9.707 4.293l-4.82-4.82A5.968 5.968 0 0014 8 6 6 0 002 8a6 6 0 006 6 5.968 5.968 0 003.473-1.113l4.82 4.82a.997.997 0 001.414 0 .999.999 0 000-1.414z"
|
||||
fill="#5C5F62"
|
||||
/></svg
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z"
|
||||
/>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex w-48 max-w-lg rounded-md shadow-sm">
|
||||
<input
|
||||
type="text"
|
||||
name="search"
|
||||
id="search"
|
||||
placeholder="search"
|
||||
autocomplete="off"
|
||||
aria-label="Search input"
|
||||
class="block w-full min-w-0 flex-1 rounded border border-zinc-700 bg-zinc-800 p-[3px] px-2 pl-10 text-zinc-200 placeholder:text-zinc-500 sm:text-sm sm:leading-6"
|
||||
style=""
|
||||
/>
|
||||
<div
|
||||
class="absolute right-1 top-1 inline-flex items-center rounded border border-zinc-700/20 bg-zinc-700/50 px-1 py-[2px] text-gray-400 shadow sm:text-sm"
|
||||
>
|
||||
⌘K
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<a href="/projects/{$project?.id}/player" class="text-zinc-400 hover:text-zinc-200">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="h-6 w-6"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M15.91 11.672a.375.375 0 010 .656l-5.603 3.113a.375.375 0 01-.557-.328V8.887c0-.286.307-.466.557-.327l5.603 3.112z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<a href="/projects/{$project?.id}/settings" class="text-zinc-400 hover:text-zinc-300">
|
||||
<div class="rounded-md p-1 hover:bg-zinc-700 hover:bg-zinc-700 hover:text-zinc-200">
|
||||
<div class="h-6 w-6 ">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="h-6 w-6"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z"
|
||||
/>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
{/if}
|
||||
|
||||
<div class="project-container h-100 flex-auto overflow-y-auto">
|
||||
<slot />
|
||||
|
@ -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<string, Promise<Record<string, Delta[]>>> = {};
|
||||
$: visibleSessions
|
||||
.filter((s) => deltasBySessionId[s.id] === undefined)
|
||||
let currentDeltas: Record<string, Promise<Record<string, Delta[]>>> = {};
|
||||
$: 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<string, Promise<Record<string, string>>> = {};
|
||||
$: if (earliestVisibleSession && docsBySessionId[earliestVisibleSession.id] === undefined) {
|
||||
docsBySessionId[earliestVisibleSession.id] = listFiles({
|
||||
type VideoFileEdit = {
|
||||
filepath: string;
|
||||
delta: Delta;
|
||||
};
|
||||
|
||||
type VideoChapter = {
|
||||
title: string;
|
||||
session: string;
|
||||
files: Record<string, number>;
|
||||
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<string, DayVideo> = {};
|
||||
let sessionFiles: Record<string, Record<string, string>> = {};
|
||||
let sessionChapters: Record<string, VideoChapter> = {};
|
||||
|
||||
$: 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<string, Delta[]>)
|
||||
);
|
||||
function max<T>(a: T, b: T): T {
|
||||
return a > b ? a : b;
|
||||
}
|
||||
|
||||
let frame: {
|
||||
function min<T>(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<typeof setInterval> | 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);
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if $sessions.length === 0}
|
||||
@ -166,74 +221,77 @@
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex h-full w-full flex-col">
|
||||
{#if frame !== null}
|
||||
<header class="shadow-md">
|
||||
<h2 class="px-4 py-2 text-xl text-zinc-300">{frame.filepath}</h2>
|
||||
</header>
|
||||
|
||||
<div
|
||||
class="project-container flex-auto overflow-y-auto overflow-x-hidden"
|
||||
style="height: calc(100vh - 270px)"
|
||||
>
|
||||
<CodeViewer filepath={frame.filepath} doc={frame.doc} deltas={frame.deltas} />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div id="timeline" class="relative w-full px-4 pb-4" bind:this={timeline}>
|
||||
<div
|
||||
id="cursor"
|
||||
use:slider
|
||||
on:drag={({ detail: v }) => (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)"
|
||||
>
|
||||
<div class="h-5 w-0.5 rounded-sm bg-white" />
|
||||
</div>
|
||||
|
||||
<div class="mb-4 flex w-full items-center justify-between">
|
||||
<div id="from">
|
||||
{new Date(minVisibleTimestamp).toLocaleString()}
|
||||
<div class="flex flex-row h-full w-full border">
|
||||
<div class="w-64 flex-shrink-0 border">
|
||||
<div>Playlist</div>
|
||||
{#each Object.entries(sessionDays) as [day, sessions]}
|
||||
<div class="flex flex-col border" on:click={selectDay(day)}>
|
||||
<div class="text-gray-500">{day}</div>
|
||||
{sessions.length}
|
||||
</div>
|
||||
|
||||
<div id="to">
|
||||
{new Date(maxVisibleTimestamp).toLocaleString()}
|
||||
{/each}
|
||||
</div>
|
||||
<div class="flex-grow">
|
||||
<div class="flex flex-col h-full w-full border">
|
||||
<div class="flex-grow border overflow-auto">
|
||||
{ymdToDate(currentDay)}
|
||||
{#if dayPlaylist[currentDay] !== undefined}
|
||||
<div>{dayPlaylist[currentDay].chapters.length} chapters</div>
|
||||
<div>{dayPlaylist[currentDay].editCount} edits</div>
|
||||
<div>{Math.round(dayPlaylist[currentDay].totalDurationMs / 1000 / 60)} min</div>
|
||||
{#if currentEdit !== null}
|
||||
<div>{currentEdit.sessionId}</div>
|
||||
<div>{currentEdit.filepath}</div>
|
||||
<div>{currentEdit.delta.timestampMs}</div>
|
||||
<div>{new Date(currentEdit.delta.timestampMs)}</div>
|
||||
<div>{currentEdit.delta.operations}</div>
|
||||
<CodeViewer
|
||||
doc={currentEdit.doc}
|
||||
deltas={[currentEdit.delta]}
|
||||
filepath={currentEdit.filepath}
|
||||
/>
|
||||
{/if}
|
||||
{:else}
|
||||
loading...
|
||||
{/if}
|
||||
<div>{currentPlayerValue}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-full px-10">
|
||||
<div id="ranges" class="flex w-full items-center gap-1" on:mousedown={onSelectTimestamp}>
|
||||
<div
|
||||
class="h-2 rounded-sm"
|
||||
style:background-color="inherit"
|
||||
style:width={rangeWidth([minVisibleTimestamp, visibleRanges[0][0]])}
|
||||
/>
|
||||
{#each visibleRanges as [from, to, filled]}
|
||||
<div
|
||||
class="h-2 rounded-sm"
|
||||
style:background-color={filled ? '#D9D9D9' : 'inherit'}
|
||||
style:width={rangeWidth([from, to])}
|
||||
/>
|
||||
{/each}
|
||||
<div
|
||||
class="h-2 rounded-sm"
|
||||
style:background-color="inherit"
|
||||
style:width={rangeWidth([
|
||||
visibleRanges[visibleRanges.length - 1][1],
|
||||
maxVisibleTimestamp
|
||||
])}
|
||||
/>
|
||||
<div class="border">Meta Data</div>
|
||||
<div class="flex flex-row border">
|
||||
<div>play</div>
|
||||
<div class="w-full">
|
||||
{#if dayPlaylist[currentDay] !== undefined}
|
||||
<input
|
||||
type="range"
|
||||
class="w-full bg-white"
|
||||
max={dayPlaylist[currentDay].editCount}
|
||||
step="1"
|
||||
bind:value={currentPlayerValue}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
<div>2x</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mx-auto flex items-center gap-2">
|
||||
{#if interval}
|
||||
<button on:click={stop}><IconPlayerPauseFilled class="h-6 w-6" /></button>
|
||||
{:else}
|
||||
<button on:click={play}><IconPlayerPlayFilled class="h-6 w-6" /></button>
|
||||
{/if}
|
||||
<button on:click={speedUp}>{speed}x</button>
|
||||
<div class="w-64 border">
|
||||
<div>Sessions</div>
|
||||
<div class="flex flex-col">
|
||||
{#each sessionDays[currentDay] as session}
|
||||
<div class="border overflow-auto">
|
||||
{new Date(session.meta.startTimestampMs).toLocaleTimeString()}
|
||||
to
|
||||
{new Date(session.meta.lastTimestampMs).toLocaleTimeString()}
|
||||
{#if sessionChapters[session.id] !== undefined}
|
||||
<div>{Math.round(sessionChapters[session.id].totalDurationMs / 1000 / 60)} min</div>
|
||||
{#each Object.entries(sessionChapters[session.id].files) as [filepath, count]}
|
||||
<div>{filepath}</div>
|
||||
<div>{count}</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
Loading…
Reference in New Issue
Block a user