Fix the rewind functionality

- Shift+Cmd+R when viewing virtual branches to switch to rewind
- Same combo to switch back to virtual branches
- Not everything is great, but it's a start
This commit is contained in:
Mattias Granlund 2023-10-12 21:24:28 +02:00
parent 885a69f8f0
commit 5dc19be8fe
22 changed files with 382 additions and 446 deletions

View File

@ -199,7 +199,7 @@
<div
id="content"
class="grid h-full w-full flex-auto select-text whitespace-pre font-mono"
class="border-color-4 grid h-full w-full flex-auto select-text whitespace-pre border-t font-mono"
style:grid-template-columns="minmax(auto, max-content) minmax(auto, max-content) 1fr"
>
{#each rows as row}
@ -211,24 +211,20 @@
row.type === RowType.Equal || row.type === RowType.Addition
? String(row.currentLineNumber)
: ''}
<span
class="select-none border-r border-light-200 bg-light-800 text-dark-300 dark:border-dark-800 dark:bg-dark-800 dark:text-light-300"
>
<span class="bg-color-3 border-color-4 text-color-1 select-none border-l border-r">
<div class="mx-1.5 text-right">
{baseNumber}
</div>
</span>
<span
class="mr-1 select-none border-r border-light-200 bg-light-800 text-dark-300 dark:border-dark-800 dark:bg-dark-800 dark:text-light-300"
>
<span class="bg-color-3 border-color-4 text-color-1 select-none border-r">
<div class="mx-1.5 text-right">
{curNumber}
</div>
</span>
<span
class="diff-line-{row.type} cursor-text overflow-hidden whitespace-pre-wrap"
class="diff-line-{row.type} bg-color-5 cursor-text overflow-hidden whitespace-pre-wrap"
class:line-changed={row.type === RowType.Addition || row.type === RowType.Deletion}
>
{#each row.render.html as content}

View File

@ -1,36 +1,29 @@
import { writable, type Loadable, derived, Loaded } from 'svelte-loadable-store';
import * as bookmarks from '$lib/api/ipc/bookmarks';
import { get as getValue, type Readable } from '@square/svelte-store';
import { type Loadable, asyncWritable, asyncDerived } from '@square/svelte-store';
const stores: Partial<Record<string, Readable<Loadable<bookmarks.Bookmark[]>>>> = {};
export function getBookmarksStore(params: {
projectId: string;
}): Readable<Loadable<bookmarks.Bookmark[]>> {
const cached = stores[params.projectId];
if (cached) return cached;
const store = writable(bookmarks.list(params), (set) => {
const unsubscribe = bookmarks.subscribe(params, (bookmark) => {
const oldValue = getValue(store);
if (oldValue.isLoading) {
bookmarks.list(params).then(set);
} else if (Loaded.isError(oldValue)) {
bookmarks.list(params).then(set);
} else {
set(oldValue.value.filter((b) => b.timestampMs !== bookmark.timestampMs).concat(bookmark));
}
});
return () => {
Promise.resolve(unsubscribe).then((unsubscribe) => unsubscribe());
};
});
stores[params.projectId] = store;
return store as Readable<Loadable<bookmarks.Bookmark[]>>;
export function getBookmarksStore(params: { projectId: string }): Loadable<bookmarks.Bookmark[]> {
return asyncWritable(
[],
async () => await bookmarks.list(params),
undefined,
{ trackState: true },
(set, update) => {
const unsubscribe = bookmarks.subscribe(params, (bookmark) => {
update((oldValue) =>
oldValue.filter((b) => b.timestampMs !== bookmark.timestampMs).concat(bookmark)
);
});
return () => {
Promise.resolve(unsubscribe).then((unsubscribe) => unsubscribe());
};
}
);
}
export function getBookmark(params: { projectId: string; timestampMs: number }) {
return derived(getBookmarksStore({ projectId: params.projectId }), (bookmarks) =>
bookmarks.find((b) => b.timestampMs === params.timestampMs)
return asyncDerived(
getBookmarksStore({ projectId: params.projectId }),
async (bookmarks) => bookmarks.find((b) => b.timestampMs === params.timestampMs),
{ trackState: true }
);
}

View File

@ -1,6 +1,5 @@
import { asyncWritable, isReloadable } from '@square/svelte-store';
import { asyncWritable, isReloadable, type AsyncWritable, type Stores } from '@square/svelte-store';
import { subscribeToDeltas, type Delta, listDeltas } from '$lib/api/ipc/deltas';
import type { Stores, Writable } from 'svelte/store';
/**
* We have a special situation here where we use deltas to know when to re-run
@ -10,20 +9,23 @@ import type { Stores, Writable } from 'svelte/store';
*/
export function getDeltasStore(
projectId: string,
sessionId: string | undefined = undefined
): Writable<Partial<Record<string, Delta[]>>> & { setSessionId: (sid: string) => void } {
sessionId: string | undefined = undefined,
subscribe = false
): AsyncWritable<Partial<Record<string, Delta[]>>> & { setSessionId: (sid: string) => void } {
let unsubscribe: () => void;
const store = asyncWritable<Stores, Partial<Record<string, Delta[]>>>(
[],
async () => {
if (!sessionId) return {};
if (unsubscribe) unsubscribe();
unsubscribe = subscribeToDeltas(projectId, sessionId, ({ filePath, deltas }) => {
store.update((storeValue) => {
storeValue[filePath] = [...(storeValue[filePath] || []), ...deltas];
return storeValue;
if (subscribe) {
unsubscribe = subscribeToDeltas(projectId, sessionId, ({ filePath, deltas }) => {
store.update((storeValue) => {
storeValue[filePath] = [...(storeValue[filePath] || []), ...deltas];
return storeValue;
});
});
});
}
return await listDeltas({ projectId, sessionId });
},
undefined,

View File

@ -1,37 +0,0 @@
import { writable, type Loadable, Loaded } from 'svelte-loadable-store';
import * as files from '$lib/api/ipc/files';
import { get, type Readable } from '@square/svelte-store';
type Files = Partial<Record<string, files.FileContent>>;
const stores: Partial<Record<string, Readable<Loadable<Files>>>> = {};
export function getFilesStore(params: {
projectId: string;
sessionId: string;
}): Readable<Loadable<Files>> {
const key = `${params.projectId}/${params.sessionId}`;
const cached = stores[key];
if (cached) return cached;
const store = writable(files.list(params), (set) => {
const unsubscribe = files.subscribe(params, ({ filePath, contents }) => {
const oldValue = get(store);
if (oldValue.isLoading) {
files.list(params).then(set);
} else if (Loaded.isError(oldValue)) {
files.list(params).then(set);
} else {
set({
...oldValue.value,
[filePath]: contents || undefined
});
}
});
return () => {
Promise.resolve(unsubscribe).then((unsubscribe) => unsubscribe());
};
});
stores[key] = store;
return store as Readable<Loadable<Files>>;
}

View File

@ -63,7 +63,7 @@
<li>
<Tooltip label="Replay">
<Button
on:click={() => goto(`/projects/${$project.id}/player`)}
on:click={() => goto(`/projects/${$project?.id}/player`)}
kind="plain"
icon={IconRewind}
/>
@ -72,7 +72,7 @@
<li>
<Tooltip label="Project settings">
<Button
on:click={() => goto(`/projects/${$project.id}/settings`)}
on:click={() => goto(`/projects/${$project?.id}/settings`)}
kind="plain"
icon={IconSettings}
/>

View File

@ -3,7 +3,6 @@ import { getSessionStore } from '$lib/stores/sessions';
import { getDiffsStore } from '$lib/api/git/diffs';
import { error } from '@sveltejs/kit';
import type { LayoutLoad } from './$types';
import type { Loadable } from '@square/svelte-store';
import { getProjectStore, type Project } from '$lib/api/ipc/projects';
export const prerender = false;
@ -15,6 +14,6 @@ export const load: LayoutLoad = async ({ params }) => {
head: getHeadStore(params.projectId),
sessions: getSessionStore(params.projectId),
diffs: getDiffsStore({ projectId: params.projectId }),
project: project as Loadable<Project> & Pick<typeof project, 'update' | 'delete'>
project: project
};
};

View File

@ -2,24 +2,18 @@
import { getTime, subDays } from 'date-fns';
import type { PageData } from './$types';
import { IconGitBranch } from '$lib/icons';
import { derived } from '@square/svelte-store';
import { asyncDerived } from '@square/svelte-store';
import FileSummaries from './FileSummaries.svelte';
import { Tooltip } from '$lib/components';
import Chat from './Chat.svelte';
export let data: PageData;
const { project, sessions, head } = data;
$: recentSessions = derived(
$: recentSessions = asyncDerived(
sessions,
(item) => {
const lastFourDaysOfSessions = item?.filter(
(result) => result.meta.startTimestampMs >= getTime(subDays(new Date(), 4))
);
if (lastFourDaysOfSessions?.length >= 4) return lastFourDaysOfSessions;
return item?.slice(0, 4).sort((a, b) => b.meta.startTimestampMs - a.meta.startTimestampMs);
},
[]
async (item) =>
item?.filter((result) => result.meta.startTimestampMs >= getTime(subDays(new Date(), 4))),
{ trackState: true }
);
</script>
@ -46,9 +40,9 @@
</div>
</div>
<div class="flex flex-auto flex-col overflow-auto">
<!-- <div class="flex flex-auto flex-col overflow-auto">
<Chat project={$project} />
</div>
</div> -->
</div>
<div class="main-content-container flex w-2/3 flex-auto flex-col">

View File

@ -2,7 +2,6 @@
import { format, startOfDay } from 'date-fns';
import type { Delta } from '$lib/api/ipc/deltas';
import { generateBuckets } from './histogram';
import { derived, Loaded } from 'svelte-loadable-store';
import FileActivity from './FileActivity.svelte';
import { page } from '$app/stores';
import { Link } from '$lib/components';
@ -10,13 +9,14 @@
import { collapse } from '$lib/paths';
import type { Session } from '$lib/api/ipc/sessions';
import { getDeltasStore } from '$lib/stores/deltas';
import { asyncDerived } from '@square/svelte-store';
export let sessions: Session[];
$: sessionDeltas = (sessions ?? []).map(({ id, projectId }) => getDeltasStore(projectId, id));
$: deltasByDate = derived(sessionDeltas, (sessionDeltas) =>
sessionDeltas.reduce(
$: deltasByDate = asyncDerived(sessionDeltas, async (sessionDeltas) => {
return sessionDeltas.reduce(
(acc, sessionDelta) => {
Object.entries(sessionDelta).forEach(([filepath, deltas]) => {
if (!deltas) return;
@ -28,10 +28,11 @@
return acc;
},
{} as Record<string, Record<string, Delta[]>>
)
);
);
});
$: deltasByDateState = deltasByDate?.state;
$: buckets = derived(sessionDeltas, (sessionDeltas) => {
$: buckets = asyncDerived(sessionDeltas, async (sessionDeltas) => {
const deltas = sessionDeltas.flatMap((deltas) => Object.values(deltas).flat() as Delta[]);
const timestamps = deltas.map((delta) => delta.timestampMs);
return generateBuckets(timestamps, 18);
@ -39,14 +40,14 @@
</script>
<ul class="mr-1 flex flex-1 flex-col space-y-4 overflow-y-auto px-8 pb-8">
{#if $deltasByDate.isLoading || $buckets.isLoading}
{#if $deltasByDate?.isLoading}
<li class="flex flex-1 space-y-4 rounded-lg border border-dashed border-zinc-400">
<div class="flex flex-1 flex-col items-center justify-center gap-4">
<IconLoading class="h-16 w-16 animate-spin text-zinc-400 " />
<h2 class="text-2xl font-bold text-zinc-400">Loading file changes...</h2>
</div>
</li>
{:else if Loaded.isError($deltasByDate) || Loaded.isError($buckets)}
{:else if $deltasByDateState?.isError}
<li class="flex flex-1 space-y-4 rounded-lg border border-dashed border-zinc-400">
<div class="flex flex-1 flex-col items-center justify-center gap-4">
<IconSparkle class="h-16 w-16 text-zinc-400 " />
@ -54,14 +55,14 @@
<p class="text-zinc-400">We couldn't load your file changes. Please try again later.</p>
</div>
</li>
{:else}
{#each Object.entries($deltasByDate.value) as [ts, fileDeltas]}
{:else if $deltasByDate}
{#each Object.entries($deltasByDate) as [ts, fileDeltas]}
{@const date = new Date(ts)}
<li class="card changed-day-card flex flex-col">
<header
class="header flex flex-row justify-between gap-2 rounded-tl rounded-tr border-b-gb-700 bg-card-active px-3 py-2"
class="header border-color-2 bg-color-2 flex flex-row justify-between gap-2 rounded-tl rounded-tr border-b px-3 py-2"
>
<div class="text-zinc-300">
<div class="text-color-3">
{date.toLocaleDateString('en-us', {
weekday: 'long',
year: 'numeric',
@ -78,7 +79,7 @@
</header>
<ul class="all-files-changed flex flex-col rounded pl-3">
{#each Object.entries(fileDeltas) as [filepath, deltas]}
<li class="changed-file flex items-center justify-between gap-4 bg-[#212121]">
<li class="changed-file bg-color-1 flex items-center justify-between gap-4">
<a
class="file-name max-w- flex w-full max-w-[360px] overflow-auto py-2 font-mono hover:underline"
href="/projects/{$page.params.projectId}/player/{format(
@ -90,19 +91,17 @@
{collapse(filepath)}
</span>
</a>
<FileActivity {deltas} buckets={$buckets.value} />
<FileActivity {deltas} buckets={$buckets} />
</li>
{/each}
</ul>
</li>
{:else}
<div
class="replay-no-changes text-center space-y-4 border dark:border-dark-300 px-10 py-12 mb-6 rounded-lg h-full flex justify-around items-center dark:text-light-400 border-light-200 text-dark-300"
class="replay-no-changes text-center space-y-4 border px-10 py-12 mb-6 rounded-lg h-full flex justify-around items-center border-color-2 text-color-2"
>
<div class="max-w-[360px] m-auto">
<h3 class="mb-6 text-3xl font-semibold dark:text-light-200 text-dark-400">
Waiting for file changes...
</h3>
<h3 class="mb-6 text-3xl font-semibold text-color-1">Waiting for file changes...</h3>
<p class="mt-1">
GitButler is now watching your project directory for file changes. As long as GitButler
is running, changes to any text files in this directory will automatically be recorded.

View File

@ -58,14 +58,15 @@
>
{#each $dates as date}
{@const isToday = format(new Date(date), 'yyyy-MM-dd') === today}
<li class="date-card">
<li class="bg-color-2 text-color-2">
<a
href="/projects/{$page.params.projectId}/player/{date}{$page.url.search}"
class:bg-card-active={date === currentDate}
class:text-white={date === currentDate}
class:border-zinc-700={date !== currentDate}
class:bg-card-default={date !== currentDate}
class="card max-h-content flex w-full flex-col items-center justify-around p-2 text-zinc-300 shadow transition duration-150 ease-out hover:bg-card-active hover:ease-in"
class:text-color-1={date == currentDate}
class:text-color-2={date != currentDate}
class:bg-color-2={date != currentDate}
class:bg-color-1={date == currentDate}
class:border-zinc-700={date != currentDate}
class="card max-h-content flex w-full flex-col items-center justify-around p-2 shadow transition duration-150 ease-out hover:ease-in"
>
{#if isToday}
<div class="py-2 text-lg leading-5">Today</div>

View File

@ -3,8 +3,8 @@ import { format, compareDesc } from 'date-fns';
import type { PageLoad } from './$types';
import { getSessionStore } from '$lib/stores/sessions';
export const load: PageLoad = async ({ url, params }) => {
const sessions = getSessionStore(params.projectId);
export const load: PageLoad = async ({ url, params, parent }) => {
const { sessions } = await parent();
const latestDate = (await sessions.load())
.map((session) => session.meta.startTimestampMs)
.sort(compareDesc)

View File

@ -1,7 +1,6 @@
<script lang="ts">
import type { LayoutData } from './$types';
import { page } from '$app/stores';
import { format } from 'date-fns';
import { onMount } from 'svelte';
import * as hotkeys from '$lib/hotkeys';
import * as events from '$lib/events';
@ -10,55 +9,21 @@
import SessionsList from './SessionsList.svelte';
import SessionNavigations from './SessionNavigations.svelte';
import { IconLoading } from '$lib/icons';
import { getSessionStore } from '$lib/stores/sessions';
import { getDeltasStore } from '$lib/stores/deltas';
import { getFilesStore } from '$lib/stores/files';
import * as bookmarks from '$lib/api/ipc/bookmarks';
import { asyncDerived } from '@square/svelte-store';
import { derived } from 'svelte/store';
import { goto } from '$app/navigation';
export let data: LayoutData;
const { currentFilepath, currentTimestamp } = data;
const {
currentSession,
richSessions,
richSessions2,
currentSessionId,
currentFilepath,
currentTimestamp
} = data;
const filter = derived(page, (page) => page.url.searchParams.get('file'));
const projectId = derived(page, (page) => page.params.projectId);
$: sessions = getSessionStore($page.params.projectId);
$: dateSessions = asyncDerived([sessions, page], async ([sessions, page]) =>
sessions
.filter((session) => format(session.meta.startTimestampMs, 'yyyy-MM-dd') === page.params.date)
.sort((a, b) => a.meta.startTimestampMs - b.meta.startTimestampMs)
);
$: richSessions = asyncDerived(
[dateSessions, projectId, filter],
async ([sessions, projectId, filter]) =>
sessions.map((session) => ({
...session,
deltas: asyncDerived(getDeltasStore(projectId, session.id), async (deltas) =>
Object.fromEntries(
Object.entries(deltas).filter(([path]) => (filter ? path === filter : true))
)
),
files: asyncDerived(
getFilesStore({ projectId: projectId, sessionId: session.id }),
async (files) =>
Object.fromEntries(
Object.entries(files).filter(([path]) => (filter ? path === filter : true))
)
)
}))
);
$: richSessionsState = richSessions.state;
$: currentSession = asyncDerived(
[page, richSessions, data.currentSessionId],
async ([page, sessions, currentSessionId]) =>
sessions.find((s) => s.id === currentSessionId) ??
sessions.find((s) => s.id === page.params.sessionId),
{ trackState: true }
);
$: currentSessionsState = sessions.state;
$: currentSessionsState = currentSession.state;
let bookmarkModal: BookmarkModal;
@ -66,6 +31,9 @@
unsubscribe(
events.on('openBookmarkModal', () => bookmarkModal?.show($currentTimestamp)),
hotkeys.on('Meta+Shift+D', () => bookmarkModal?.show($currentTimestamp)),
hotkeys.on('Meta+Shift+R', () =>
goto(location.href.replace('/projects/', '/repo/').replace(/\/player.*/, ''))
),
hotkeys.on('D', async () => {
const existing = await bookmarks.list({
projectId: $page.params.projectId,
@ -104,15 +72,15 @@
</div>
{:else}
<SessionsList
sessions={$richSessions}
currentSession={$currentSession}
sessions={$richSessions2}
currentSessionId={$currentSessionId}
currentFilepath={$currentFilepath}
/>
{/if}
</article>
<div id="player" class="card relative my-2 flex flex-auto flex-col overflow-auto">
<header class="flex items-center gap-3 bg-card-active px-3 py-2">
<header class="bg-color-3 flex items-center gap-3 px-3 py-2">
{#if $currentSessionsState?.isLoading || $richSessionsState?.isLoading}
<span>Loading...</span>
{:else if $currentSessionsState?.isError || $richSessionsState?.isError}
@ -120,7 +88,7 @@
{:else if !$currentSession}
<span>No session found</span>
{:else}
<SessionNavigations currentSession={$currentSession} sessions={$richSessions} />
<SessionNavigations currentSession={$currentSession} sessions={$richSessions2} />
{/if}
</header>

View File

@ -1,8 +1,136 @@
import { writable } from '@square/svelte-store';
import { asyncDerived, derived, writable } from '@square/svelte-store';
import type { LayoutLoad } from './$types';
import { format } from 'date-fns';
import { page } from '$app/stores';
import { listDeltas, type Delta } from '$lib/api/ipc/deltas';
import { list } from '$lib/api/ipc/files';
export const load: LayoutLoad = () => ({
currentFilepath: writable(''),
currentSessionId: writable(''),
currentTimestamp: writable(-1)
});
export const load: LayoutLoad = async ({ parent, params, url }) => {
const { sessions } = await parent();
const dateSessions = asyncDerived([sessions], async ([sessions]) => {
return sessions.filter(
(session) => format(session.meta.startTimestampMs, 'yyyy-MM-dd') === params.date
);
});
const filter = writable(url.searchParams.get('file'));
const projectId = writable(params.projectId);
const loadedDeltas = asyncDerived(dateSessions, async (sessions) => {
return Promise.all(
sessions.map(async (s) => {
return {
sessionId: s.id,
deltas: await listDeltas({ projectId: params.projectId, sessionId: s.id })
};
})
);
});
const loadedDateDeltas = asyncDerived(loadedDeltas, async (deltas) => {
const deltas2 = deltas.map((s) => {
return {
sessionId: s.sessionId,
deltas: Object.entries(s.deltas)
.flatMap(([path, deltas]) =>
(deltas || []).map((delta) => [path, delta] as [string, Delta])
)
.sort((a, b) => a[1].timestampMs - b[1].timestampMs)
};
});
const deltasMap: { [k: string]: [string, Delta][] } = {};
for (let i = 0; i < deltas2.length; i++) {
const deltas = deltas2[i].deltas;
if (deltas && Object.keys(deltas)) {
deltasMap[deltas2[i].sessionId] = deltas2[i].deltas;
}
}
return deltasMap;
});
const loadedDateDeltas2 = asyncDerived(loadedDeltas, async (deltas) => {
const deltasMap: { [k: string]: Partial<Record<string, Delta[]>> } = {};
for (let i = 0; i < deltas.length; i++) {
if (deltas && Object.keys(deltas)) {
deltasMap[deltas[i].sessionId] = deltas[i].deltas;
}
}
return deltasMap;
});
const loadedFiles = asyncDerived(loadedDateDeltas2, async (deltas) => {
const sessionIds = Object.keys(deltas);
const files = sessionIds.map(async (sessionId) => {
const filenames = Object.keys(deltas[sessionId] || {});
const p = { projectId: params.projectId, sessionId: sessionId, paths: filenames };
return {
sessionId: sessionId,
files: Object.fromEntries(
Object.entries(await list(p)).map(([path, file]) => {
if (file?.type === 'utf8') {
return [path, file.value];
} else {
return [path, undefined];
}
})
)
};
});
const resolvedFiles = await Promise.all(files);
const filesMap: { [y: string]: { [k: string]: string | undefined } } = {};
for (let i = 0; i < resolvedFiles.length; i++) {
const files = resolvedFiles[i].files;
if (files && Object.keys(files)) {
filesMap[resolvedFiles[i].sessionId] = files;
}
}
return filesMap;
});
const richSessions = asyncDerived(
[dateSessions, loadedDateDeltas, loadedFiles, projectId, filter],
async ([sessions, loadedDateDeltas, loadedFiles]) => {
return sessions.map((session) => ({
...session,
deltas: loadedDateDeltas[session.id],
files: loadedFiles[session.id]
}));
}
);
const richSessions2 = asyncDerived(
[dateSessions, loadedDateDeltas2, loadedFiles],
async ([sessions, loadedDateDeltas, loadedFiles]) =>
sessions.map((session) => ({
...session,
deltas: loadedDateDeltas[session.id],
files: loadedFiles[session.id]
}))
);
const currentSessionId = writable('');
const currentSession = derived(
[page, richSessions, currentSessionId],
([page, richSessions, currentSessionId]) => {
const val =
richSessions?.find((s) => s.id === currentSessionId) ??
richSessions?.find((s) => s.id === page.params.sessionId);
return val;
}
);
return {
currentFilepath: writable(''),
currentTimestamp: writable(-1),
currentSessionId,
dateSessions,
richSessions,
richSessions2,
filter,
projectId,
currentSession,
loadedDateDeltas,
loadedDeltas,
loadedFiles,
sessions
};
};

View File

@ -3,12 +3,11 @@
import { isInsert, type Delta, isDelete } from '$lib/api/ipc/deltas';
import { page } from '$app/stores';
import { collapse } from '$lib/paths';
import { derived } from '@square/svelte-store';
import { asyncDerived } from '@square/svelte-store';
import { getBookmarksStore } from '$lib/stores/bookmarks';
import { IconBookmarkFilled } from '$lib/icons';
import { line } from '$lib/diff';
import { Stats } from '$lib/components';
import { Loaded } from 'svelte-loadable-store';
export let isCurrent: boolean;
export let session: Session;
@ -54,18 +53,19 @@
})
.reduce((a, b) => [a[0] + b[0], a[1] + b[1]], [0, 0]);
$: bookmarksStore = derived(getBookmarksStore({ projectId: session.projectId }), (bookmarks) => {
if (bookmarks.isLoading) return [];
if (Loaded.isError(bookmarks)) return [];
const timestamps = Object.values(deltas ?? {}).flatMap((deltas) =>
(deltas || []).map((d) => d.timestampMs)
);
const start = Math.min(...timestamps);
const end = Math.max(...timestamps);
return bookmarks.value
.filter((bookmark) => !bookmark.deleted)
.filter((bookmark) => bookmark.timestampMs >= start && bookmark.timestampMs < end);
});
$: bookmarksStore = asyncDerived(
getBookmarksStore({ projectId: session.projectId }),
async (bookmarks) => {
const timestamps = Object.values(deltas ?? {}).flatMap((deltas) =>
(deltas || []).map((d) => d.timestampMs)
);
const start = Math.min(...timestamps);
const end = Math.max(...timestamps);
return bookmarks
.filter((bookmark) => !bookmark.deleted)
.filter((bookmark) => bookmark.timestampMs >= start && bookmark.timestampMs < end);
}
);
const unique = (value: any, index: number, self: any[]) => self.indexOf(value) === index;
const lexically = (a: string, b: string) => a.localeCompare(b);
@ -110,8 +110,8 @@
<li
bind:this={card}
id={isCurrent ? 'current-session' : ''}
class:bg-card-active={isCurrent}
class="session-card relative rounded border-[0.5px] border-gb-700 text-zinc-300 shadow-md transition-colors duration-200 ease-in-out hover:bg-card-active"
class:bg-color-4={isCurrent}
class="session-card border-color-2 text-color-2 hover:bg-color-4 relative rounded border-[0.5px] shadow-md transition-colors duration-200 ease-in-out"
>
{#await bookmarksStore.load() then}
{#if $bookmarksStore?.length > 0}
@ -136,14 +136,13 @@
{#if isCurrent}
<ul
class="list-disk list-none overflow-hidden rounded-bl rounded-br bg-zinc-800 py-1 pl-0 pr-2"
style:list-style="disc"
class="list-disk bg-color-2 list-none overflow-hidden rounded-bl rounded-br py-1 pl-0 pr-2"
>
{#each changedFiles.sort(lexically) as filename}
<li
class:text-zinc-100={currentFilepath === filename}
class:bg-[#3356C2]={currentFilepath === filename}
class="mx-5 ml-1 w-full list-none rounded p-1 text-zinc-500"
class="text-color-3 mx-5 ml-1 w-full list-none rounded p-1"
>
{collapse(filename)}
</li>

View File

@ -7,41 +7,37 @@
import type { Session } from '$lib/api/ipc/sessions';
import type { Delta } from '$lib/api/ipc/deltas';
import { unsubscribe } from '$lib/utils';
import { derived, type Readable } from '@square/svelte-store';
import { onMount } from 'svelte';
import { format } from 'date-fns';
export let sessions: (Session & {
deltas: Readable<Partial<Record<string, Delta[]>>>;
deltas: Partial<Record<string, Delta[]>>;
})[];
export let currentSession: Session;
$: sessionDeltas = derived(
sessions.map(({ deltas }) => deltas),
(deltas) => deltas
);
let nextSessionId: string | undefined;
let prevSessionId: string | undefined;
$: nextSessionId = derived(sessionDeltas, (sessionDeltas) => {
if (sessions) {
const currentIndex = sessions.findIndex((s) => s.id === currentSession.id);
if (currentIndex === -1) return undefined;
for (let i = currentIndex + 1; i < sessions.length; i++) {
if (Object.keys(sessionDeltas[i]).length > 0) return sessions[i].id;
}
return undefined;
}
});
$: sessionDeltas = sessions.map(({ deltas }) => deltas);
$: prevSessionId = derived(sessionDeltas, (sessionDeltas) => {
if (sessions) {
const currentIndex = sessions.findIndex((s) => s.id === currentSession.id);
if (currentIndex === -1) return undefined;
for (let i = currentIndex - 1; i >= 0; i--) {
if (Object.keys(sessionDeltas[i]).length > 0) return sessions[i].id;
$: if (sessions && currentSession) {
const currentIndex = sessions.findIndex((s) => s.id === currentSession.id);
nextSessionId = undefined;
for (let i = currentIndex + 1; i < sessions.length; i++) {
if (Object.keys(sessionDeltas[i]).length > 0) {
nextSessionId = sessions[i].id;
break;
}
return undefined;
}
});
prevSessionId = undefined;
for (let i = currentIndex - 1; i >= 0; i--) {
if (Object.keys(sessionDeltas[i]).length > 0) {
prevSessionId = sessions[i].id;
break;
}
}
}
const removeFromSearchParams = (params: URLSearchParams, key: string) => {
params.delete(key);
@ -56,10 +52,10 @@
onMount(() =>
unsubscribe(
hotkeys.on('Shift+ArrowRight', () => {
if ($nextSessionId) goto(getSessionURI($nextSessionId));
if (nextSessionId) goto(getSessionURI(nextSessionId));
}),
hotkeys.on('Shift+ArrowLeft', () => {
if ($prevSessionId) goto(getSessionURI($prevSessionId));
if (prevSessionId) goto(getSessionURI(prevSessionId));
})
)
);
@ -72,24 +68,22 @@
</span>
<div class="flex items-center gap-1">
{#if $prevSessionId && $nextSessionId}
<a
href={$prevSessionId && getSessionURI($prevSessionId)}
class="rounded border border-zinc-500 bg-zinc-600 p-0.5"
class:hover:bg-zinc-500={!!$prevSessionId}
class:pointer-events-none={!$prevSessionId}
class:text-zinc-500={!$prevSessionId}
>
<IconChevronLeft class="h-4 w-4" />
</a>
<a
href={$nextSessionId && getSessionURI($nextSessionId)}
class="rounded border border-zinc-500 bg-zinc-600 p-0.5"
class:hover:bg-zinc-500={!!$nextSessionId}
class:pointer-events-none={!$nextSessionId}
class:text-zinc-500={!$nextSessionId}
>
<IconChevronRight class="h-4 w-4" />
</a>
{/if}
<a
href={prevSessionId && getSessionURI(prevSessionId)}
class="bg-color-4 rounded border p-0.5"
class:hover:bg-color-5={!!prevSessionId}
class:pointer-events-none={!prevSessionId}
class:text-color-4={!prevSessionId}
>
<IconChevronLeft class="h-4 w-4" />
</a>
<a
href={nextSessionId && getSessionURI(nextSessionId)}
class="bg-color-4 rounded border p-0.5"
class:hover:bg-color-5={!!nextSessionId}
class:pointer-events-none={!nextSessionId}
class:text-color-4={!nextSessionId}
>
<IconChevronRight class="h-4 w-4" />
</a>
</div>

View File

@ -1,55 +1,45 @@
<script lang="ts">
import type { Session } from '$lib/api/ipc/sessions';
import type { Delta } from '$lib/api/ipc/deltas';
import { asyncDerived, type Readable } from '@square/svelte-store';
import SessionCard from './SessionCard.svelte';
export let sessions: (Session & {
deltas: Readable<Partial<Record<string, Delta[]>>>;
files: Readable<Partial<Record<string, string>>>;
deltas: Partial<Record<string, Delta[]>>;
files: Partial<Record<string, string>>;
})[];
export let currentSession: Session | undefined;
export let currentSessionId: string | undefined;
export let currentFilepath: string;
$: visibleDeltas = asyncDerived(
sessions.map(({ deltas }) => deltas),
async (deltas) => deltas.map((delta) => Object.fromEntries(Object.entries(delta ?? {})))
);
$: visibleFiles = asyncDerived(
sessions.map(({ files }) => files),
async (files) => files.map((file) => Object.fromEntries(Object.entries(file ?? {})))
);
$: visibleDeltas = sessions?.map((s) => s.deltas);
$: visibleFiles = sessions?.map((s) => s.files);
$: visibleSessions = sessions?.map((session, i) => ({
...session,
visible: Object.keys($visibleDeltas[i]).length > 0
visible: Object.keys(visibleDeltas[i]).length > 0
}));
</script>
<header
class="card-header flex flex-row justify-between rounded-t border-b-[1px] border-b-divider bg-card-active px-3 py-2 leading-[21px]"
class="card-header bg-color-2 flex flex-row justify-between rounded-t px-3 py-2 leading-[21px]"
>
<div class="relative flex gap-2">
<div class="relative bottom-[1px] h-4 w-4 text-sm">🧰</div>
<div>Working History</div>
<div class="text-zinc-400">
{visibleSessions.filter(({ visible }) => visible).length}
{visibleSessions?.filter(({ visible }) => visible).length}
</div>
</div>
</header>
<ul
class="mr-1 flex h-full flex-col gap-2 overflow-auto rounded-b bg-card-default pb-2 pl-2 pr-1 pt-2"
>
{#each visibleSessions as session, i}
{@const isCurrent = session.id === currentSession?.id}
{#if session.visible && $visibleDeltas && $visibleFiles}
<ul class="bg-color-3 mr-1 flex h-full flex-col gap-2 overflow-auto rounded-b pb-2 pl-2 pr-1 pt-2">
{#each visibleSessions || [] as session, i}
{@const isCurrent = session.id == currentSessionId}
{#if session.visible && visibleDeltas?.length > 0 && visibleFiles?.length > 0}
<SessionCard
{isCurrent}
{session}
deltas={$visibleDeltas[i]}
files={$visibleFiles[i]}
deltas={visibleDeltas[i]}
files={visibleFiles[i]}
{currentFilepath}
/>
{/if}

View File

@ -2,77 +2,26 @@
import Slider from './Slider.svelte';
import type { PageData } from './$types';
import { page } from '$app/stores';
import { get, writable } from '@square/svelte-store';
import { derived, Loaded } from 'svelte-loadable-store';
import { format } from 'date-fns';
import { writable } from '@square/svelte-store';
import Playback from './Playback.svelte';
import type { Frame as FrameType } from './frame';
import Frame from './Frame.svelte';
import Info from './Info.svelte';
import type { Delta } from '$lib/api/ipc/deltas';
import { getSessionStore } from '$lib/stores/sessions';
import { getDeltasStore } from '$lib/stores/deltas';
import { getFilesStore } from '$lib/stores/files';
import { getBookmarksStore } from '$lib/stores/bookmarks';
export let data: PageData;
const { currentFilepath, currentTimestamp, currentSessionId } = data;
const { currentFilepath, currentTimestamp, richSessions, currentSessionId } = data;
let fullContext = true;
let context = 8;
page.subscribe((page) => {
currentDeltaIndex = parseInt(page.url.searchParams.get('delta') || '0');
currentSessionId.set(page.params.sessionId);
});
const filter = derived(page, (page) => page.url.searchParams.get('file'));
const projectId = derived(page, (page) => page.params.projectId);
$: bookmarks = getBookmarksStore({ projectId: $page.params.projectId });
$: sessions = getSessionStore($page.params.projectId);
$: dateSessions = derived([sessions, page], async ([sessions, page]) =>
sessions?.filter(
(session) => format(session.meta.startTimestampMs, 'yyyy-MM-dd') === page.params.date
)
);
$: richSessions = derived([dateSessions, filter, projectId], ([sessions, filter, projectId]) =>
sessions.map((session) => ({
...session,
deltas: derived(getDeltasStore(projectId, session.id), (deltas) =>
Object.entries(deltas)
.filter(([path]) => (filter ? path === filter : true))
.flatMap(([path, deltas]) =>
(deltas || []).map((delta) => [path, delta] as [string, Delta])
)
.sort((a, b) => a[1].timestampMs - b[1].timestampMs)
),
files: derived(getFilesStore({ projectId, sessionId: session.id }), (files) =>
Object.fromEntries(
Object.entries(files)
.filter(([path]) => (filter ? path === filter : true))
.map(([path, file]) => {
if (file?.type === 'utf8') {
return [path, file.value];
} else {
return [path, undefined];
}
})
)
)
}))
);
$: richSessionsState = richSessions?.state;
$: currentDeltaIndex = parseInt($page.url.searchParams.get('delta') || '0');
richSessions?.subscribe((sessions) => {
if (sessions.isLoading) return;
if (Loaded.isError(sessions)) return;
if (sessions.value.length === 0) return;
if (!sessions.value.some((s) => s.id === $currentSessionId)) {
$currentSessionId = sessions.value[0].id;
}
});
$: if ($page.params.sessionId) {
currentSessionId.set($page.params.sessionId);
}
let frame: FrameType | null = null;
@ -84,35 +33,28 @@
const value = writable(0);
$: {
$: if ($richSessions) {
// this hook updates player value if current page url has changed
if (!$richSessions.isLoading && Loaded.isValue($richSessions)) {
const currentSessionIndex = $richSessions.value.findIndex(
(s) => s.id === $page.params.sessionId
);
$value =
$richSessions.value
.filter((_, index) => index < currentSessionIndex)
.reduce((acc, s) => {
const deltas = get(s.deltas);
if (!deltas.isLoading && Loaded.isValue(deltas)) {
return acc + deltas.value.length;
} else {
return acc;
}
}, 0) + currentDeltaIndex;
}
const currentSessionIndex = $richSessions.findIndex((s) => {
return s.id == $page.params.sessionId;
});
$value =
$richSessions
.filter((_, index) => index < currentSessionIndex)
.reduce((acc, s) => {
return acc + s.deltas.length;
}, 0) + currentDeltaIndex;
}
</script>
{#if $richSessions.isLoading}
{#if $richSessionsState?.isLoading}
<div class="flex h-full flex-col items-center justify-center">
<div
class="loader border-gray-200 mb-4 h-12 w-12 rounded-full border-4 border-t-4 ease-linear"
/>
<h2 class="text-center text-2xl font-medium text-gray-500">Loading...</h2>
</div>
{:else if Loaded.isError($richSessions)}
{:else if $richSessionsState?.isError}
<div class="flex h-full flex-col items-center justify-center">
<h2 class="text-center text-2xl font-medium text-gray-500">Something went wrong</h2>
</div>
@ -120,15 +62,9 @@
<Frame
{context}
{fullContext}
sessions={$richSessions.value}
deltas={derived(
$richSessions.value.map(({ deltas }) => deltas),
(deltas) => deltas
)}
files={derived(
$richSessions.value.map(({ files }) => files),
(files) => files
)}
sessions={$richSessions}
deltas={$richSessions?.map((s) => s.deltas)}
files={$richSessions?.map((s) => s.files)}
bind:frame
value={$value}
/>
@ -139,30 +75,14 @@
</div>
{/if}
<div class="flex-shrink flex-grow"></div>
<div
id="controls"
class="absolute bottom-0 flex w-full flex-col gap-4 overflow-hidden rounded-bl rounded-br border-t border-zinc-700 bg-[#2E2E32]/75 p-2 pt-4"
style="
border-width: 0.5px;
-webkit-backdrop-filter: blur(5px) saturate(190%) contrast(70%) brightness(80%);
backdrop-filter: blur(5px) saturate(190%) contrast(70%) brightness(80%);
background-color: rgba(24, 24, 27, 0.60);
border: 0.5px solid rgba(63, 63, 70, 0.50);
"
class="border-color-4 bg-color-3 bottom-0 flex w-full flex-col gap-4 rounded-bl rounded-br border-t p-2 pt-4"
>
<Slider
sessions={derived(
$richSessions.value.map(({ deltas }) => deltas),
(deltas) => deltas
)}
{bookmarks}
bind:value={$value}
/>
<Slider sessions={$richSessions?.map(({ deltas }) => deltas)} {bookmarks} bind:value={$value} />
<Playback
deltas={derived(
$richSessions.value.map(({ deltas }) => deltas),
(deltas) => deltas
)}
deltas={$richSessions?.map(({ deltas }) => deltas)}
bind:value={$value}
bind:context
bind:fullContext

View File

@ -3,27 +3,21 @@
import type { Delta } from '$lib/api/ipc/deltas';
import type { Frame } from './frame';
import { DeltasViewer } from '$lib/components';
import type { Readable } from '@square/svelte-store';
import { Loaded, type Loadable } from 'svelte-loadable-store';
import type { Loadable, Readable } from '@square/svelte-store';
export let context: number;
export let fullContext: boolean;
export let sessions: Session[];
export let deltas: Readable<Loadable<[string, Delta][][]>>;
export let files: Readable<Loadable<Partial<Record<string, string>>[]>>;
export let deltas: [string, Delta][][];
export let files: Partial<Record<string, string>>[];
export let value: number;
export let frame: Frame | null = null;
$: {
if (
!$deltas.isLoading &&
!$files.isLoading &&
Loaded.isValue($deltas) &&
Loaded.isValue($files)
) {
if (deltas && files) {
let i = value;
for (const j in $deltas.value) {
const dd = $deltas.value[j];
for (const j in deltas) {
const dd = deltas[j];
if (i < dd.length) {
const frameDeltas = dd.slice(0, i + 1);
const frameFilepath = frameDeltas[frameDeltas.length - 1][0];
@ -34,7 +28,7 @@
.sort((a, b) => a[1].timestampMs - b[1].timestampMs)
.map((delta) => delta[1]),
filepath: frameFilepath,
doc: $files.value[j][frameFilepath] || ''
doc: files[j][frameFilepath] || ''
};
break;
}
@ -45,15 +39,13 @@
</script>
{#if frame}
<div id="code" class="flex-auto overflow-auto bg-[#1E2021]">
<div class="pb-[200px]">
<DeltasViewer
doc={frame.doc}
deltas={frame.deltas}
filepath={frame.filepath}
paddingLines={fullContext ? 100000 : context}
/>
</div>
<div id="code" class="overflow-auto">
<DeltasViewer
doc={frame.doc}
deltas={frame.deltas}
filepath={frame.filepath}
paddingLines={fullContext ? 100000 : context}
/>
</div>
{:else}
<div class="mt-8 text-center">Select a playlist</div>

View File

@ -4,7 +4,6 @@
import { IconBookmark, IconBookmarkFilled } from '$lib/icons';
import { format } from 'date-fns';
import { page } from '$app/stores';
import { Loaded } from 'svelte-loadable-store';
import * as bookmarks from '$lib/api/ipc/bookmarks';
import { getBookmark } from '$lib/stores/bookmarks';
@ -12,12 +11,13 @@
export let filename: string;
$: bookmark = getBookmark({ projectId: $page.params.projectId, timestampMs });
$: bookmarkState = bookmark.state;
const toggleBookmark = () => {
if ($bookmark.isLoading) return;
if (Loaded.isError($bookmark)) return;
if ($bookmarkState?.isLoading) return;
if ($bookmarkState?.isError) return;
bookmarks.upsert(
!$bookmark.value
!$bookmark
? {
projectId: $page.params.projectId,
timestampMs,
@ -25,14 +25,14 @@
deleted: false
}
: {
...$bookmark.value,
deleted: !$bookmark.value.deleted
...$bookmark,
deleted: !$bookmark.deleted
}
);
};
</script>
{#if !$bookmark.isLoading && !Loaded.isError($bookmark)}
{#if !$bookmarkState?.isLoading && !$bookmarkState?.isError}
<div
class="flex max-w-[357px] flex-col gap-2 rounded-[18px] px-4 py-2 shadow"
style="border: 0.5px solid rgba(63, 63, 70, 0.5);
@ -45,9 +45,9 @@
{collapse(filename)}
</span>
<button on:click={toggleBookmark} class="z-1">
{#if $bookmark.value?.deleted}
{#if $bookmark?.deleted}
<IconBookmark class="h-4 w-4 text-zinc-700" />
{:else if !$bookmark.value}
{:else if !$bookmark}
<IconBookmark class="h-4 w-4 text-zinc-700" />
{:else}
<IconBookmarkFilled class="h-4 w-4 text-bookmark-selected" />
@ -55,7 +55,7 @@
</button>
</header>
{#if $bookmark.value && $bookmark.value.note.length && !$bookmark.value.deleted}
{#if $bookmark && $bookmark.note.length && !$bookmark.deleted}
<div
role="button"
tabindex="0"
@ -64,11 +64,11 @@
on:keydown={() => events.emit('openBookmarkModal')}
>
<main class="max-h-[7ch] overflow-auto text-text-subdued">
{$bookmark.value.note}
{$bookmark.note}
</main>
<footer class="text-right text-sm text-text-subdued">
{format(new Date($bookmark.value.updatedTimestampMs), 'E d MMM yyyy')}
{format(new Date($bookmark.updatedTimestampMs), 'E d MMM yyyy')}
</footer>
</div>
{/if}

View File

@ -5,15 +5,13 @@
import { unsubscribe } from '$lib/utils';
import { onMount } from 'svelte';
import * as hotkeys from '$lib/hotkeys';
import type { Readable } from '@square/svelte-store';
import { type Loadable, derived, Loaded } from 'svelte-loadable-store';
export let value: number;
export let context: number;
export let fullContext: boolean;
export let deltas: Readable<Loadable<[string, Delta][][]>>;
export let deltas: [string, Delta][][];
$: maxDeltaIndex = derived(deltas, (deltas) => deltas.flatMap((d) => d).length - 1);
$: maxDeltaIndex = deltas?.flatMap((d) => d).length - 1;
let interval: ReturnType<typeof setInterval> | undefined;
let direction: -1 | 1 = 1;
@ -38,9 +36,7 @@
};
const gotoNextDelta = () => {
if ($maxDeltaIndex.isLoading) return;
if (Loaded.isError($maxDeltaIndex)) return;
if (value < $maxDeltaIndex.value) {
if (value < maxDeltaIndex) {
value += 1;
} else {
stop();
@ -102,7 +98,7 @@
<div class="back-forward-button-container">
<button
on:click={gotoPrevDelta}
class="player-button-back group duration-300 ease-in-out hover:scale-125"
class="player-button-back text-color-4 hover:text-color-3 group duration-300 ease-in-out hover:scale-125"
>
<svg
width="20"
@ -116,15 +112,14 @@
fill-rule="evenodd"
clip-rule="evenodd"
d="M13.7101 16.32C14.0948 16.7047 14.0955 17.3274 13.7117 17.7111C13.3254 18.0975 12.7053 18.094 12.3206 17.7093L5.37536 10.7641C5.18243 10.5711 5.0867 10.32 5.08703 10.069C5.08802 9.81734 5.18374 9.56621 5.37536 9.37458L12.3206 2.42932C12.7055 2.04445 13.328 2.04396 13.7117 2.42751C14.0981 2.81386 14.0946 3.43408 13.7101 3.81863C13.4234 4.10528 7.80387 9.78949 7.52438 10.069C9.59011 12.1474 11.637 14.2469 13.7101 16.32Z"
fill="none"
class="fill-zinc-400 group-hover:fill-zinc-100"
fill="currentColor"
/>
</svg>
</button>
<button
on:click={gotoNextDelta}
class="player-button-forward group duration-300 ease-in-out hover:scale-125"
class="player-button-forward text-color-4 hover:text-color-3 duration-300 ease-in-out hover:scale-125"
>
<svg
width="20"
@ -138,8 +133,7 @@
fill-rule="evenodd"
clip-rule="evenodd"
d="M6.28991 16.32C5.90521 16.7047 5.90455 17.3274 6.28826 17.7111C6.67461 18.0975 7.29466 18.094 7.67938 17.7093L14.6246 10.7641C14.8176 10.5711 14.9133 10.32 14.913 10.069C14.912 9.81734 14.8163 9.56621 14.6246 9.37458L7.67938 2.42932C7.29451 2.04445 6.67197 2.04396 6.28826 2.42751C5.90192 2.81386 5.90537 3.43408 6.28991 3.81863C6.57656 4.10528 12.1961 9.78949 12.4756 10.069C10.4099 12.1474 8.36301 14.2469 6.28991 16.32Z"
fill="none"
class="fill-zinc-400 group-hover:fill-zinc-100"
fill="currentColor"
/>
</svg>
</button>

View File

@ -1,20 +1,19 @@
<script lang="ts">
import type { Delta } from '$lib/api/ipc/deltas';
import type { Bookmark } from '$lib/api/ipc/bookmarks';
import { derived, Loaded, type Loadable } from 'svelte-loadable-store';
import type { Readable } from '@square/svelte-store';
import { ModuleChapters, ModuleMarkers, type Marker } from './slider';
import { JSR, ModuleSlider } from 'mm-jsr';
import { asyncDerived, type Loadable } from '@square/svelte-store';
export let sessions: Readable<Loadable<[string, Delta][][]>>;
export let sessions: [string, Delta][][];
export let value: number;
export let bookmarks: Readable<Loadable<Bookmark[]>>;
export let bookmarks: Loadable<Bookmark[]>;
$: bookmarkedTimestamps = derived(bookmarks, (bookmarks) =>
$: bookmarkedTimestamps = asyncDerived(bookmarks, async (bookmarks) =>
bookmarks.filter(({ deleted }) => !deleted).map((bookmark) => bookmark.timestampMs)
);
$: markers = derived([sessions, bookmarkedTimestamps], ([sessions, bookmarkedTimestamps]) =>
$: markers = asyncDerived([bookmarkedTimestamps], async ([bookmarkedTimestamps]) =>
sessions.flatMap((session, index, all) => {
const from = all.slice(0, index).reduce((acc, deltas) => acc + deltas.length, 0);
return session
@ -27,17 +26,13 @@
})
);
$: totalDeltas = derived(sessions, (sessions) =>
sessions.reduce((acc, deltas) => acc + deltas.length, 0)
);
$: totalDeltas = sessions?.reduce((acc, deltas) => acc + deltas.length, 0);
$: chapters = derived(sessions, (sessions) =>
sessions.map((session, index, all) => {
const from = all.slice(0, index).reduce((acc, deltas) => acc + deltas.length, 0);
const to = from + session.length;
return [from, to] as [number, number];
})
);
$: chapters = sessions?.map((session, index, all) => {
const from = all.slice(0, index).reduce((acc, deltas) => acc + deltas.length, 0);
const to = from + session.length;
return [from, to] as [number, number];
});
type Config = {
min: number;
@ -90,14 +85,15 @@
};
</script>
{#if !$totalDeltas.isLoading && Loaded.isValue($totalDeltas) && !$chapters.isLoading && Loaded.isValue($chapters) && !$markers.isLoading && Loaded.isValue($markers)}
{#if totalDeltas && chapters && $markers}
<div
class="bg-color-1 rounded"
use:jsrSlider={{
min: 0,
max: $totalDeltas.value,
max: totalDeltas,
initialValue: value,
chapters: $chapters.value,
markers: $markers.value
chapters: chapters,
markers: $markers
}}
>
<style>

View File

@ -4,7 +4,7 @@
import type { PageData } from './$types';
import { Button, Link } from '$lib/components';
import { BranchController } from '$lib/vbranches/branchController';
import { getContext } from 'svelte';
import { getContext, onMount } from 'svelte';
import { SETTINGS_CONTEXT, type SettingsStore } from '$lib/userSettings';
import { IconExternalLink } from '$lib/icons';
import {
@ -24,6 +24,8 @@
import IconChevronLeft from '$lib/icons/IconChevronLeft.svelte';
import { goto } from '$app/navigation';
import BaseBranchSelect from './BaseBranchSelect.svelte';
import { unsubscribe } from '$lib/utils';
import * as hotkeys from '$lib/hotkeys';
export let data: PageData;
let { projectId, project, cloud } = data;
@ -31,7 +33,7 @@
const userSettings = getContext<SettingsStore>(SETTINGS_CONTEXT);
const fetchStore = getFetchesStore(projectId);
const deltasStore = getDeltasStore(projectId);
const deltasStore = getDeltasStore(projectId, undefined, true);
const headStore = getHeadsStore(projectId);
const sessionsStore = getSessionStore(projectId);
const baseBranchStore = getBaseBranchStore(projectId, fetchStore, headStore);
@ -71,6 +73,14 @@
function updateDeltasStore(sid: string | undefined) {
if (sid) deltasStore.setSessionId(sid);
}
onMount(() =>
unsubscribe(
hotkeys.on('Meta+Shift+R', () =>
goto(location.href.replace('/repo/', '/projects/') + '/player')
)
)
);
</script>
{#if $baseBranchesState.isLoading}

View File

@ -35,8 +35,6 @@
export let selectable = false;
export let selectedOwnership: Writable<Ownership>;
$: console.log(file)
const userSettings = getContext<SettingsStore>(SETTINGS_CONTEXT);
const dispatch = createEventDispatcher<{
expanded: boolean;