mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-12-02 07:53:55 +03:00
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:
parent
885a69f8f0
commit
5dc19be8fe
@ -199,7 +199,7 @@
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
id="content"
|
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"
|
style:grid-template-columns="minmax(auto, max-content) minmax(auto, max-content) 1fr"
|
||||||
>
|
>
|
||||||
{#each rows as row}
|
{#each rows as row}
|
||||||
@ -211,24 +211,20 @@
|
|||||||
row.type === RowType.Equal || row.type === RowType.Addition
|
row.type === RowType.Equal || row.type === RowType.Addition
|
||||||
? String(row.currentLineNumber)
|
? String(row.currentLineNumber)
|
||||||
: ''}
|
: ''}
|
||||||
<span
|
<span class="bg-color-3 border-color-4 text-color-1 select-none border-l border-r">
|
||||||
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"
|
|
||||||
>
|
|
||||||
<div class="mx-1.5 text-right">
|
<div class="mx-1.5 text-right">
|
||||||
{baseNumber}
|
{baseNumber}
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span
|
<span class="bg-color-3 border-color-4 text-color-1 select-none border-r">
|
||||||
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"
|
|
||||||
>
|
|
||||||
<div class="mx-1.5 text-right">
|
<div class="mx-1.5 text-right">
|
||||||
{curNumber}
|
{curNumber}
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<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}
|
class:line-changed={row.type === RowType.Addition || row.type === RowType.Deletion}
|
||||||
>
|
>
|
||||||
{#each row.render.html as content}
|
{#each row.render.html as content}
|
||||||
|
@ -1,36 +1,29 @@
|
|||||||
import { writable, type Loadable, derived, Loaded } from 'svelte-loadable-store';
|
|
||||||
import * as bookmarks from '$lib/api/ipc/bookmarks';
|
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 }): Loadable<bookmarks.Bookmark[]> {
|
||||||
|
return asyncWritable(
|
||||||
export function getBookmarksStore(params: {
|
[],
|
||||||
projectId: string;
|
async () => await bookmarks.list(params),
|
||||||
}): Readable<Loadable<bookmarks.Bookmark[]>> {
|
undefined,
|
||||||
const cached = stores[params.projectId];
|
{ trackState: true },
|
||||||
if (cached) return cached;
|
(set, update) => {
|
||||||
|
|
||||||
const store = writable(bookmarks.list(params), (set) => {
|
|
||||||
const unsubscribe = bookmarks.subscribe(params, (bookmark) => {
|
const unsubscribe = bookmarks.subscribe(params, (bookmark) => {
|
||||||
const oldValue = getValue(store);
|
update((oldValue) =>
|
||||||
if (oldValue.isLoading) {
|
oldValue.filter((b) => b.timestampMs !== bookmark.timestampMs).concat(bookmark)
|
||||||
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 () => {
|
return () => {
|
||||||
Promise.resolve(unsubscribe).then((unsubscribe) => unsubscribe());
|
Promise.resolve(unsubscribe).then((unsubscribe) => unsubscribe());
|
||||||
};
|
};
|
||||||
});
|
}
|
||||||
stores[params.projectId] = store;
|
);
|
||||||
return store as Readable<Loadable<bookmarks.Bookmark[]>>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getBookmark(params: { projectId: string; timestampMs: number }) {
|
export function getBookmark(params: { projectId: string; timestampMs: number }) {
|
||||||
return derived(getBookmarksStore({ projectId: params.projectId }), (bookmarks) =>
|
return asyncDerived(
|
||||||
bookmarks.find((b) => b.timestampMs === params.timestampMs)
|
getBookmarksStore({ projectId: params.projectId }),
|
||||||
|
async (bookmarks) => bookmarks.find((b) => b.timestampMs === params.timestampMs),
|
||||||
|
{ trackState: true }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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 { 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
|
* 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(
|
export function getDeltasStore(
|
||||||
projectId: string,
|
projectId: string,
|
||||||
sessionId: string | undefined = undefined
|
sessionId: string | undefined = undefined,
|
||||||
): Writable<Partial<Record<string, Delta[]>>> & { setSessionId: (sid: string) => void } {
|
subscribe = false
|
||||||
|
): AsyncWritable<Partial<Record<string, Delta[]>>> & { setSessionId: (sid: string) => void } {
|
||||||
let unsubscribe: () => void;
|
let unsubscribe: () => void;
|
||||||
const store = asyncWritable<Stores, Partial<Record<string, Delta[]>>>(
|
const store = asyncWritable<Stores, Partial<Record<string, Delta[]>>>(
|
||||||
[],
|
[],
|
||||||
async () => {
|
async () => {
|
||||||
if (!sessionId) return {};
|
if (!sessionId) return {};
|
||||||
if (unsubscribe) unsubscribe();
|
if (unsubscribe) unsubscribe();
|
||||||
|
if (subscribe) {
|
||||||
unsubscribe = subscribeToDeltas(projectId, sessionId, ({ filePath, deltas }) => {
|
unsubscribe = subscribeToDeltas(projectId, sessionId, ({ filePath, deltas }) => {
|
||||||
store.update((storeValue) => {
|
store.update((storeValue) => {
|
||||||
storeValue[filePath] = [...(storeValue[filePath] || []), ...deltas];
|
storeValue[filePath] = [...(storeValue[filePath] || []), ...deltas];
|
||||||
return storeValue;
|
return storeValue;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
}
|
||||||
return await listDeltas({ projectId, sessionId });
|
return await listDeltas({ projectId, sessionId });
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
|
@ -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>>;
|
|
||||||
}
|
|
@ -63,7 +63,7 @@
|
|||||||
<li>
|
<li>
|
||||||
<Tooltip label="Replay">
|
<Tooltip label="Replay">
|
||||||
<Button
|
<Button
|
||||||
on:click={() => goto(`/projects/${$project.id}/player`)}
|
on:click={() => goto(`/projects/${$project?.id}/player`)}
|
||||||
kind="plain"
|
kind="plain"
|
||||||
icon={IconRewind}
|
icon={IconRewind}
|
||||||
/>
|
/>
|
||||||
@ -72,7 +72,7 @@
|
|||||||
<li>
|
<li>
|
||||||
<Tooltip label="Project settings">
|
<Tooltip label="Project settings">
|
||||||
<Button
|
<Button
|
||||||
on:click={() => goto(`/projects/${$project.id}/settings`)}
|
on:click={() => goto(`/projects/${$project?.id}/settings`)}
|
||||||
kind="plain"
|
kind="plain"
|
||||||
icon={IconSettings}
|
icon={IconSettings}
|
||||||
/>
|
/>
|
||||||
|
@ -3,7 +3,6 @@ import { getSessionStore } from '$lib/stores/sessions';
|
|||||||
import { getDiffsStore } from '$lib/api/git/diffs';
|
import { getDiffsStore } from '$lib/api/git/diffs';
|
||||||
import { error } from '@sveltejs/kit';
|
import { error } from '@sveltejs/kit';
|
||||||
import type { LayoutLoad } from './$types';
|
import type { LayoutLoad } from './$types';
|
||||||
import type { Loadable } from '@square/svelte-store';
|
|
||||||
import { getProjectStore, type Project } from '$lib/api/ipc/projects';
|
import { getProjectStore, type Project } from '$lib/api/ipc/projects';
|
||||||
|
|
||||||
export const prerender = false;
|
export const prerender = false;
|
||||||
@ -15,6 +14,6 @@ export const load: LayoutLoad = async ({ params }) => {
|
|||||||
head: getHeadStore(params.projectId),
|
head: getHeadStore(params.projectId),
|
||||||
sessions: getSessionStore(params.projectId),
|
sessions: getSessionStore(params.projectId),
|
||||||
diffs: getDiffsStore({ projectId: params.projectId }),
|
diffs: getDiffsStore({ projectId: params.projectId }),
|
||||||
project: project as Loadable<Project> & Pick<typeof project, 'update' | 'delete'>
|
project: project
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -2,24 +2,18 @@
|
|||||||
import { getTime, subDays } from 'date-fns';
|
import { getTime, subDays } from 'date-fns';
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
import { IconGitBranch } from '$lib/icons';
|
import { IconGitBranch } from '$lib/icons';
|
||||||
import { derived } from '@square/svelte-store';
|
import { asyncDerived } from '@square/svelte-store';
|
||||||
import FileSummaries from './FileSummaries.svelte';
|
import FileSummaries from './FileSummaries.svelte';
|
||||||
import { Tooltip } from '$lib/components';
|
import { Tooltip } from '$lib/components';
|
||||||
import Chat from './Chat.svelte';
|
|
||||||
|
|
||||||
export let data: PageData;
|
export let data: PageData;
|
||||||
const { project, sessions, head } = data;
|
const { project, sessions, head } = data;
|
||||||
|
|
||||||
$: recentSessions = derived(
|
$: recentSessions = asyncDerived(
|
||||||
sessions,
|
sessions,
|
||||||
(item) => {
|
async (item) =>
|
||||||
const lastFourDaysOfSessions = item?.filter(
|
item?.filter((result) => result.meta.startTimestampMs >= getTime(subDays(new Date(), 4))),
|
||||||
(result) => result.meta.startTimestampMs >= getTime(subDays(new Date(), 4))
|
{ trackState: true }
|
||||||
);
|
|
||||||
if (lastFourDaysOfSessions?.length >= 4) return lastFourDaysOfSessions;
|
|
||||||
return item?.slice(0, 4).sort((a, b) => b.meta.startTimestampMs - a.meta.startTimestampMs);
|
|
||||||
},
|
|
||||||
[]
|
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -46,9 +40,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-auto flex-col overflow-auto">
|
<!-- <div class="flex flex-auto flex-col overflow-auto">
|
||||||
<Chat project={$project} />
|
<Chat project={$project} />
|
||||||
</div>
|
</div> -->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="main-content-container flex w-2/3 flex-auto flex-col">
|
<div class="main-content-container flex w-2/3 flex-auto flex-col">
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
import { format, startOfDay } from 'date-fns';
|
import { format, startOfDay } from 'date-fns';
|
||||||
import type { Delta } from '$lib/api/ipc/deltas';
|
import type { Delta } from '$lib/api/ipc/deltas';
|
||||||
import { generateBuckets } from './histogram';
|
import { generateBuckets } from './histogram';
|
||||||
import { derived, Loaded } from 'svelte-loadable-store';
|
|
||||||
import FileActivity from './FileActivity.svelte';
|
import FileActivity from './FileActivity.svelte';
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import { Link } from '$lib/components';
|
import { Link } from '$lib/components';
|
||||||
@ -10,13 +9,14 @@
|
|||||||
import { collapse } from '$lib/paths';
|
import { collapse } from '$lib/paths';
|
||||||
import type { Session } from '$lib/api/ipc/sessions';
|
import type { Session } from '$lib/api/ipc/sessions';
|
||||||
import { getDeltasStore } from '$lib/stores/deltas';
|
import { getDeltasStore } from '$lib/stores/deltas';
|
||||||
|
import { asyncDerived } from '@square/svelte-store';
|
||||||
|
|
||||||
export let sessions: Session[];
|
export let sessions: Session[];
|
||||||
|
|
||||||
$: sessionDeltas = (sessions ?? []).map(({ id, projectId }) => getDeltasStore(projectId, id));
|
$: sessionDeltas = (sessions ?? []).map(({ id, projectId }) => getDeltasStore(projectId, id));
|
||||||
|
|
||||||
$: deltasByDate = derived(sessionDeltas, (sessionDeltas) =>
|
$: deltasByDate = asyncDerived(sessionDeltas, async (sessionDeltas) => {
|
||||||
sessionDeltas.reduce(
|
return sessionDeltas.reduce(
|
||||||
(acc, sessionDelta) => {
|
(acc, sessionDelta) => {
|
||||||
Object.entries(sessionDelta).forEach(([filepath, deltas]) => {
|
Object.entries(sessionDelta).forEach(([filepath, deltas]) => {
|
||||||
if (!deltas) return;
|
if (!deltas) return;
|
||||||
@ -28,10 +28,11 @@
|
|||||||
return acc;
|
return acc;
|
||||||
},
|
},
|
||||||
{} as Record<string, Record<string, Delta[]>>
|
{} 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 deltas = sessionDeltas.flatMap((deltas) => Object.values(deltas).flat() as Delta[]);
|
||||||
const timestamps = deltas.map((delta) => delta.timestampMs);
|
const timestamps = deltas.map((delta) => delta.timestampMs);
|
||||||
return generateBuckets(timestamps, 18);
|
return generateBuckets(timestamps, 18);
|
||||||
@ -39,14 +40,14 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ul class="mr-1 flex flex-1 flex-col space-y-4 overflow-y-auto px-8 pb-8">
|
<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">
|
<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">
|
<div class="flex flex-1 flex-col items-center justify-center gap-4">
|
||||||
<IconLoading class="h-16 w-16 animate-spin text-zinc-400 " />
|
<IconLoading class="h-16 w-16 animate-spin text-zinc-400 " />
|
||||||
<h2 class="text-2xl font-bold text-zinc-400">Loading file changes...</h2>
|
<h2 class="text-2xl font-bold text-zinc-400">Loading file changes...</h2>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</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">
|
<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">
|
<div class="flex flex-1 flex-col items-center justify-center gap-4">
|
||||||
<IconSparkle class="h-16 w-16 text-zinc-400 " />
|
<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>
|
<p class="text-zinc-400">We couldn't load your file changes. Please try again later.</p>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
{:else}
|
{:else if $deltasByDate}
|
||||||
{#each Object.entries($deltasByDate.value) as [ts, fileDeltas]}
|
{#each Object.entries($deltasByDate) as [ts, fileDeltas]}
|
||||||
{@const date = new Date(ts)}
|
{@const date = new Date(ts)}
|
||||||
<li class="card changed-day-card flex flex-col">
|
<li class="card changed-day-card flex flex-col">
|
||||||
<header
|
<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', {
|
{date.toLocaleDateString('en-us', {
|
||||||
weekday: 'long',
|
weekday: 'long',
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
@ -78,7 +79,7 @@
|
|||||||
</header>
|
</header>
|
||||||
<ul class="all-files-changed flex flex-col rounded pl-3">
|
<ul class="all-files-changed flex flex-col rounded pl-3">
|
||||||
{#each Object.entries(fileDeltas) as [filepath, deltas]}
|
{#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
|
<a
|
||||||
class="file-name max-w- flex w-full max-w-[360px] overflow-auto py-2 font-mono hover:underline"
|
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(
|
href="/projects/{$page.params.projectId}/player/{format(
|
||||||
@ -90,19 +91,17 @@
|
|||||||
{collapse(filepath)}
|
{collapse(filepath)}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
<FileActivity {deltas} buckets={$buckets.value} />
|
<FileActivity {deltas} buckets={$buckets} />
|
||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
{:else}
|
{:else}
|
||||||
<div
|
<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">
|
<div class="max-w-[360px] m-auto">
|
||||||
<h3 class="mb-6 text-3xl font-semibold dark:text-light-200 text-dark-400">
|
<h3 class="mb-6 text-3xl font-semibold text-color-1">Waiting for file changes...</h3>
|
||||||
Waiting for file changes...
|
|
||||||
</h3>
|
|
||||||
<p class="mt-1">
|
<p class="mt-1">
|
||||||
GitButler is now watching your project directory for file changes. As long as GitButler
|
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.
|
is running, changes to any text files in this directory will automatically be recorded.
|
||||||
|
@ -58,14 +58,15 @@
|
|||||||
>
|
>
|
||||||
{#each $dates as date}
|
{#each $dates as date}
|
||||||
{@const isToday = format(new Date(date), 'yyyy-MM-dd') === today}
|
{@const isToday = format(new Date(date), 'yyyy-MM-dd') === today}
|
||||||
<li class="date-card">
|
<li class="bg-color-2 text-color-2">
|
||||||
<a
|
<a
|
||||||
href="/projects/{$page.params.projectId}/player/{date}{$page.url.search}"
|
href="/projects/{$page.params.projectId}/player/{date}{$page.url.search}"
|
||||||
class:bg-card-active={date === currentDate}
|
class:text-color-1={date == currentDate}
|
||||||
class:text-white={date === currentDate}
|
class:text-color-2={date != currentDate}
|
||||||
class:border-zinc-700={date !== currentDate}
|
class:bg-color-2={date != currentDate}
|
||||||
class:bg-card-default={date !== currentDate}
|
class:bg-color-1={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: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}
|
{#if isToday}
|
||||||
<div class="py-2 text-lg leading-5">Today</div>
|
<div class="py-2 text-lg leading-5">Today</div>
|
||||||
|
@ -3,8 +3,8 @@ import { format, compareDesc } from 'date-fns';
|
|||||||
import type { PageLoad } from './$types';
|
import type { PageLoad } from './$types';
|
||||||
import { getSessionStore } from '$lib/stores/sessions';
|
import { getSessionStore } from '$lib/stores/sessions';
|
||||||
|
|
||||||
export const load: PageLoad = async ({ url, params }) => {
|
export const load: PageLoad = async ({ url, params, parent }) => {
|
||||||
const sessions = getSessionStore(params.projectId);
|
const { sessions } = await parent();
|
||||||
const latestDate = (await sessions.load())
|
const latestDate = (await sessions.load())
|
||||||
.map((session) => session.meta.startTimestampMs)
|
.map((session) => session.meta.startTimestampMs)
|
||||||
.sort(compareDesc)
|
.sort(compareDesc)
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { LayoutData } from './$types';
|
import type { LayoutData } from './$types';
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import { format } from 'date-fns';
|
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import * as hotkeys from '$lib/hotkeys';
|
import * as hotkeys from '$lib/hotkeys';
|
||||||
import * as events from '$lib/events';
|
import * as events from '$lib/events';
|
||||||
@ -10,55 +9,21 @@
|
|||||||
import SessionsList from './SessionsList.svelte';
|
import SessionsList from './SessionsList.svelte';
|
||||||
import SessionNavigations from './SessionNavigations.svelte';
|
import SessionNavigations from './SessionNavigations.svelte';
|
||||||
import { IconLoading } from '$lib/icons';
|
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 * as bookmarks from '$lib/api/ipc/bookmarks';
|
||||||
import { asyncDerived } from '@square/svelte-store';
|
import { goto } from '$app/navigation';
|
||||||
import { derived } from 'svelte/store';
|
|
||||||
|
|
||||||
export let data: LayoutData;
|
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;
|
$: richSessionsState = richSessions.state;
|
||||||
|
$: currentSessionsState = currentSession.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;
|
|
||||||
|
|
||||||
let bookmarkModal: BookmarkModal;
|
let bookmarkModal: BookmarkModal;
|
||||||
|
|
||||||
@ -66,6 +31,9 @@
|
|||||||
unsubscribe(
|
unsubscribe(
|
||||||
events.on('openBookmarkModal', () => bookmarkModal?.show($currentTimestamp)),
|
events.on('openBookmarkModal', () => bookmarkModal?.show($currentTimestamp)),
|
||||||
hotkeys.on('Meta+Shift+D', () => 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 () => {
|
hotkeys.on('D', async () => {
|
||||||
const existing = await bookmarks.list({
|
const existing = await bookmarks.list({
|
||||||
projectId: $page.params.projectId,
|
projectId: $page.params.projectId,
|
||||||
@ -104,15 +72,15 @@
|
|||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<SessionsList
|
<SessionsList
|
||||||
sessions={$richSessions}
|
sessions={$richSessions2}
|
||||||
currentSession={$currentSession}
|
currentSessionId={$currentSessionId}
|
||||||
currentFilepath={$currentFilepath}
|
currentFilepath={$currentFilepath}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<div id="player" class="card relative my-2 flex flex-auto flex-col overflow-auto">
|
<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}
|
{#if $currentSessionsState?.isLoading || $richSessionsState?.isLoading}
|
||||||
<span>Loading...</span>
|
<span>Loading...</span>
|
||||||
{:else if $currentSessionsState?.isError || $richSessionsState?.isError}
|
{:else if $currentSessionsState?.isError || $richSessionsState?.isError}
|
||||||
@ -120,7 +88,7 @@
|
|||||||
{:else if !$currentSession}
|
{:else if !$currentSession}
|
||||||
<span>No session found</span>
|
<span>No session found</span>
|
||||||
{:else}
|
{:else}
|
||||||
<SessionNavigations currentSession={$currentSession} sessions={$richSessions} />
|
<SessionNavigations currentSession={$currentSession} sessions={$richSessions2} />
|
||||||
{/if}
|
{/if}
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
@ -1,8 +1,136 @@
|
|||||||
import { writable } from '@square/svelte-store';
|
import { asyncDerived, derived, writable } from '@square/svelte-store';
|
||||||
import type { LayoutLoad } from './$types';
|
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 = () => ({
|
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(''),
|
currentFilepath: writable(''),
|
||||||
currentSessionId: writable(''),
|
currentTimestamp: writable(-1),
|
||||||
currentTimestamp: writable(-1)
|
currentSessionId,
|
||||||
});
|
dateSessions,
|
||||||
|
richSessions,
|
||||||
|
richSessions2,
|
||||||
|
filter,
|
||||||
|
projectId,
|
||||||
|
currentSession,
|
||||||
|
loadedDateDeltas,
|
||||||
|
loadedDeltas,
|
||||||
|
loadedFiles,
|
||||||
|
sessions
|
||||||
|
};
|
||||||
|
};
|
||||||
|
@ -3,12 +3,11 @@
|
|||||||
import { isInsert, type Delta, isDelete } from '$lib/api/ipc/deltas';
|
import { isInsert, type Delta, isDelete } from '$lib/api/ipc/deltas';
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import { collapse } from '$lib/paths';
|
import { collapse } from '$lib/paths';
|
||||||
import { derived } from '@square/svelte-store';
|
import { asyncDerived } from '@square/svelte-store';
|
||||||
import { getBookmarksStore } from '$lib/stores/bookmarks';
|
import { getBookmarksStore } from '$lib/stores/bookmarks';
|
||||||
import { IconBookmarkFilled } from '$lib/icons';
|
import { IconBookmarkFilled } from '$lib/icons';
|
||||||
import { line } from '$lib/diff';
|
import { line } from '$lib/diff';
|
||||||
import { Stats } from '$lib/components';
|
import { Stats } from '$lib/components';
|
||||||
import { Loaded } from 'svelte-loadable-store';
|
|
||||||
|
|
||||||
export let isCurrent: boolean;
|
export let isCurrent: boolean;
|
||||||
export let session: Session;
|
export let session: Session;
|
||||||
@ -54,18 +53,19 @@
|
|||||||
})
|
})
|
||||||
.reduce((a, b) => [a[0] + b[0], a[1] + b[1]], [0, 0]);
|
.reduce((a, b) => [a[0] + b[0], a[1] + b[1]], [0, 0]);
|
||||||
|
|
||||||
$: bookmarksStore = derived(getBookmarksStore({ projectId: session.projectId }), (bookmarks) => {
|
$: bookmarksStore = asyncDerived(
|
||||||
if (bookmarks.isLoading) return [];
|
getBookmarksStore({ projectId: session.projectId }),
|
||||||
if (Loaded.isError(bookmarks)) return [];
|
async (bookmarks) => {
|
||||||
const timestamps = Object.values(deltas ?? {}).flatMap((deltas) =>
|
const timestamps = Object.values(deltas ?? {}).flatMap((deltas) =>
|
||||||
(deltas || []).map((d) => d.timestampMs)
|
(deltas || []).map((d) => d.timestampMs)
|
||||||
);
|
);
|
||||||
const start = Math.min(...timestamps);
|
const start = Math.min(...timestamps);
|
||||||
const end = Math.max(...timestamps);
|
const end = Math.max(...timestamps);
|
||||||
return bookmarks.value
|
return bookmarks
|
||||||
.filter((bookmark) => !bookmark.deleted)
|
.filter((bookmark) => !bookmark.deleted)
|
||||||
.filter((bookmark) => bookmark.timestampMs >= start && bookmark.timestampMs < end);
|
.filter((bookmark) => bookmark.timestampMs >= start && bookmark.timestampMs < end);
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const unique = (value: any, index: number, self: any[]) => self.indexOf(value) === index;
|
const unique = (value: any, index: number, self: any[]) => self.indexOf(value) === index;
|
||||||
const lexically = (a: string, b: string) => a.localeCompare(b);
|
const lexically = (a: string, b: string) => a.localeCompare(b);
|
||||||
@ -110,8 +110,8 @@
|
|||||||
<li
|
<li
|
||||||
bind:this={card}
|
bind:this={card}
|
||||||
id={isCurrent ? 'current-session' : ''}
|
id={isCurrent ? 'current-session' : ''}
|
||||||
class:bg-card-active={isCurrent}
|
class:bg-color-4={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="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}
|
{#await bookmarksStore.load() then}
|
||||||
{#if $bookmarksStore?.length > 0}
|
{#if $bookmarksStore?.length > 0}
|
||||||
@ -136,14 +136,13 @@
|
|||||||
|
|
||||||
{#if isCurrent}
|
{#if isCurrent}
|
||||||
<ul
|
<ul
|
||||||
class="list-disk list-none overflow-hidden rounded-bl rounded-br bg-zinc-800 py-1 pl-0 pr-2"
|
class="list-disk bg-color-2 list-none overflow-hidden rounded-bl rounded-br py-1 pl-0 pr-2"
|
||||||
style:list-style="disc"
|
|
||||||
>
|
>
|
||||||
{#each changedFiles.sort(lexically) as filename}
|
{#each changedFiles.sort(lexically) as filename}
|
||||||
<li
|
<li
|
||||||
class:text-zinc-100={currentFilepath === filename}
|
class:text-zinc-100={currentFilepath === filename}
|
||||||
class:bg-[#3356C2]={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)}
|
{collapse(filename)}
|
||||||
</li>
|
</li>
|
||||||
|
@ -7,41 +7,37 @@
|
|||||||
import type { Session } from '$lib/api/ipc/sessions';
|
import type { Session } from '$lib/api/ipc/sessions';
|
||||||
import type { Delta } from '$lib/api/ipc/deltas';
|
import type { Delta } from '$lib/api/ipc/deltas';
|
||||||
import { unsubscribe } from '$lib/utils';
|
import { unsubscribe } from '$lib/utils';
|
||||||
import { derived, type Readable } from '@square/svelte-store';
|
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
|
|
||||||
export let sessions: (Session & {
|
export let sessions: (Session & {
|
||||||
deltas: Readable<Partial<Record<string, Delta[]>>>;
|
deltas: Partial<Record<string, Delta[]>>;
|
||||||
})[];
|
})[];
|
||||||
export let currentSession: Session;
|
export let currentSession: Session;
|
||||||
|
|
||||||
$: sessionDeltas = derived(
|
let nextSessionId: string | undefined;
|
||||||
sessions.map(({ deltas }) => deltas),
|
let prevSessionId: string | undefined;
|
||||||
(deltas) => deltas
|
|
||||||
);
|
|
||||||
|
|
||||||
$: nextSessionId = derived(sessionDeltas, (sessionDeltas) => {
|
$: sessionDeltas = sessions.map(({ deltas }) => deltas);
|
||||||
if (sessions) {
|
|
||||||
|
$: if (sessions && currentSession) {
|
||||||
const currentIndex = sessions.findIndex((s) => s.id === currentSession.id);
|
const currentIndex = sessions.findIndex((s) => s.id === currentSession.id);
|
||||||
if (currentIndex === -1) return undefined;
|
nextSessionId = undefined;
|
||||||
for (let i = currentIndex + 1; i < sessions.length; i++) {
|
for (let i = currentIndex + 1; i < sessions.length; i++) {
|
||||||
if (Object.keys(sessionDeltas[i]).length > 0) return sessions[i].id;
|
if (Object.keys(sessionDeltas[i]).length > 0) {
|
||||||
|
nextSessionId = sessions[i].id;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
return undefined;
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
$: prevSessionId = derived(sessionDeltas, (sessionDeltas) => {
|
prevSessionId = undefined;
|
||||||
if (sessions) {
|
|
||||||
const currentIndex = sessions.findIndex((s) => s.id === currentSession.id);
|
|
||||||
if (currentIndex === -1) return undefined;
|
|
||||||
for (let i = currentIndex - 1; i >= 0; i--) {
|
for (let i = currentIndex - 1; i >= 0; i--) {
|
||||||
if (Object.keys(sessionDeltas[i]).length > 0) return sessions[i].id;
|
if (Object.keys(sessionDeltas[i]).length > 0) {
|
||||||
|
prevSessionId = sessions[i].id;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return undefined;
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
const removeFromSearchParams = (params: URLSearchParams, key: string) => {
|
const removeFromSearchParams = (params: URLSearchParams, key: string) => {
|
||||||
params.delete(key);
|
params.delete(key);
|
||||||
@ -56,10 +52,10 @@
|
|||||||
onMount(() =>
|
onMount(() =>
|
||||||
unsubscribe(
|
unsubscribe(
|
||||||
hotkeys.on('Shift+ArrowRight', () => {
|
hotkeys.on('Shift+ArrowRight', () => {
|
||||||
if ($nextSessionId) goto(getSessionURI($nextSessionId));
|
if (nextSessionId) goto(getSessionURI(nextSessionId));
|
||||||
}),
|
}),
|
||||||
hotkeys.on('Shift+ArrowLeft', () => {
|
hotkeys.on('Shift+ArrowLeft', () => {
|
||||||
if ($prevSessionId) goto(getSessionURI($prevSessionId));
|
if (prevSessionId) goto(getSessionURI(prevSessionId));
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@ -72,24 +68,22 @@
|
|||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
{#if $prevSessionId && $nextSessionId}
|
|
||||||
<a
|
<a
|
||||||
href={$prevSessionId && getSessionURI($prevSessionId)}
|
href={prevSessionId && getSessionURI(prevSessionId)}
|
||||||
class="rounded border border-zinc-500 bg-zinc-600 p-0.5"
|
class="bg-color-4 rounded border p-0.5"
|
||||||
class:hover:bg-zinc-500={!!$prevSessionId}
|
class:hover:bg-color-5={!!prevSessionId}
|
||||||
class:pointer-events-none={!$prevSessionId}
|
class:pointer-events-none={!prevSessionId}
|
||||||
class:text-zinc-500={!$prevSessionId}
|
class:text-color-4={!prevSessionId}
|
||||||
>
|
>
|
||||||
<IconChevronLeft class="h-4 w-4" />
|
<IconChevronLeft class="h-4 w-4" />
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href={$nextSessionId && getSessionURI($nextSessionId)}
|
href={nextSessionId && getSessionURI(nextSessionId)}
|
||||||
class="rounded border border-zinc-500 bg-zinc-600 p-0.5"
|
class="bg-color-4 rounded border p-0.5"
|
||||||
class:hover:bg-zinc-500={!!$nextSessionId}
|
class:hover:bg-color-5={!!nextSessionId}
|
||||||
class:pointer-events-none={!$nextSessionId}
|
class:pointer-events-none={!nextSessionId}
|
||||||
class:text-zinc-500={!$nextSessionId}
|
class:text-color-4={!nextSessionId}
|
||||||
>
|
>
|
||||||
<IconChevronRight class="h-4 w-4" />
|
<IconChevronRight class="h-4 w-4" />
|
||||||
</a>
|
</a>
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,55 +1,45 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Session } from '$lib/api/ipc/sessions';
|
import type { Session } from '$lib/api/ipc/sessions';
|
||||||
import type { Delta } from '$lib/api/ipc/deltas';
|
import type { Delta } from '$lib/api/ipc/deltas';
|
||||||
import { asyncDerived, type Readable } from '@square/svelte-store';
|
|
||||||
import SessionCard from './SessionCard.svelte';
|
import SessionCard from './SessionCard.svelte';
|
||||||
|
|
||||||
export let sessions: (Session & {
|
export let sessions: (Session & {
|
||||||
deltas: Readable<Partial<Record<string, Delta[]>>>;
|
deltas: Partial<Record<string, Delta[]>>;
|
||||||
files: Readable<Partial<Record<string, string>>>;
|
files: Partial<Record<string, string>>;
|
||||||
})[];
|
})[];
|
||||||
export let currentSession: Session | undefined;
|
export let currentSessionId: string | undefined;
|
||||||
export let currentFilepath: string;
|
export let currentFilepath: string;
|
||||||
|
|
||||||
$: visibleDeltas = asyncDerived(
|
$: visibleDeltas = sessions?.map((s) => s.deltas);
|
||||||
sessions.map(({ deltas }) => deltas),
|
$: visibleFiles = sessions?.map((s) => s.files);
|
||||||
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 ?? {})))
|
|
||||||
);
|
|
||||||
|
|
||||||
$: visibleSessions = sessions?.map((session, i) => ({
|
$: visibleSessions = sessions?.map((session, i) => ({
|
||||||
...session,
|
...session,
|
||||||
visible: Object.keys($visibleDeltas[i]).length > 0
|
visible: Object.keys(visibleDeltas[i]).length > 0
|
||||||
}));
|
}));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<header
|
<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 flex gap-2">
|
||||||
<div class="relative bottom-[1px] h-4 w-4 text-sm">🧰</div>
|
<div class="relative bottom-[1px] h-4 w-4 text-sm">🧰</div>
|
||||||
<div>Working History</div>
|
<div>Working History</div>
|
||||||
<div class="text-zinc-400">
|
<div class="text-zinc-400">
|
||||||
{visibleSessions.filter(({ visible }) => visible).length}
|
{visibleSessions?.filter(({ visible }) => visible).length}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<ul
|
<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">
|
||||||
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 == currentSessionId}
|
||||||
{#each visibleSessions as session, i}
|
{#if session.visible && visibleDeltas?.length > 0 && visibleFiles?.length > 0}
|
||||||
{@const isCurrent = session.id === currentSession?.id}
|
|
||||||
{#if session.visible && $visibleDeltas && $visibleFiles}
|
|
||||||
<SessionCard
|
<SessionCard
|
||||||
{isCurrent}
|
{isCurrent}
|
||||||
{session}
|
{session}
|
||||||
deltas={$visibleDeltas[i]}
|
deltas={visibleDeltas[i]}
|
||||||
files={$visibleFiles[i]}
|
files={visibleFiles[i]}
|
||||||
{currentFilepath}
|
{currentFilepath}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -2,77 +2,26 @@
|
|||||||
import Slider from './Slider.svelte';
|
import Slider from './Slider.svelte';
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import { get, writable } from '@square/svelte-store';
|
import { writable } from '@square/svelte-store';
|
||||||
import { derived, Loaded } from 'svelte-loadable-store';
|
|
||||||
import { format } from 'date-fns';
|
|
||||||
import Playback from './Playback.svelte';
|
import Playback from './Playback.svelte';
|
||||||
import type { Frame as FrameType } from './frame';
|
import type { Frame as FrameType } from './frame';
|
||||||
import Frame from './Frame.svelte';
|
import Frame from './Frame.svelte';
|
||||||
import Info from './Info.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';
|
import { getBookmarksStore } from '$lib/stores/bookmarks';
|
||||||
|
|
||||||
export let data: PageData;
|
export let data: PageData;
|
||||||
const { currentFilepath, currentTimestamp, currentSessionId } = data;
|
const { currentFilepath, currentTimestamp, richSessions, currentSessionId } = data;
|
||||||
|
|
||||||
let fullContext = true;
|
let fullContext = true;
|
||||||
let context = 8;
|
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 });
|
$: bookmarks = getBookmarksStore({ projectId: $page.params.projectId });
|
||||||
$: sessions = getSessionStore($page.params.projectId);
|
$: richSessionsState = richSessions?.state;
|
||||||
$: 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];
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
|
|
||||||
$: currentDeltaIndex = parseInt($page.url.searchParams.get('delta') || '0');
|
$: currentDeltaIndex = parseInt($page.url.searchParams.get('delta') || '0');
|
||||||
|
$: if ($page.params.sessionId) {
|
||||||
richSessions?.subscribe((sessions) => {
|
currentSessionId.set($page.params.sessionId);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
let frame: FrameType | null = null;
|
let frame: FrameType | null = null;
|
||||||
|
|
||||||
@ -84,35 +33,28 @@
|
|||||||
|
|
||||||
const value = writable(0);
|
const value = writable(0);
|
||||||
|
|
||||||
$: {
|
$: if ($richSessions) {
|
||||||
// this hook updates player value if current page url has changed
|
// this hook updates player value if current page url has changed
|
||||||
if (!$richSessions.isLoading && Loaded.isValue($richSessions)) {
|
const currentSessionIndex = $richSessions.findIndex((s) => {
|
||||||
const currentSessionIndex = $richSessions.value.findIndex(
|
return s.id == $page.params.sessionId;
|
||||||
(s) => s.id === $page.params.sessionId
|
});
|
||||||
);
|
|
||||||
$value =
|
$value =
|
||||||
$richSessions.value
|
$richSessions
|
||||||
.filter((_, index) => index < currentSessionIndex)
|
.filter((_, index) => index < currentSessionIndex)
|
||||||
.reduce((acc, s) => {
|
.reduce((acc, s) => {
|
||||||
const deltas = get(s.deltas);
|
return acc + s.deltas.length;
|
||||||
if (!deltas.isLoading && Loaded.isValue(deltas)) {
|
|
||||||
return acc + deltas.value.length;
|
|
||||||
} else {
|
|
||||||
return acc;
|
|
||||||
}
|
|
||||||
}, 0) + currentDeltaIndex;
|
}, 0) + currentDeltaIndex;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $richSessions.isLoading}
|
{#if $richSessionsState?.isLoading}
|
||||||
<div class="flex h-full flex-col items-center justify-center">
|
<div class="flex h-full flex-col items-center justify-center">
|
||||||
<div
|
<div
|
||||||
class="loader border-gray-200 mb-4 h-12 w-12 rounded-full border-4 border-t-4 ease-linear"
|
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>
|
<h2 class="text-center text-2xl font-medium text-gray-500">Loading...</h2>
|
||||||
</div>
|
</div>
|
||||||
{:else if Loaded.isError($richSessions)}
|
{:else if $richSessionsState?.isError}
|
||||||
<div class="flex h-full flex-col items-center justify-center">
|
<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>
|
<h2 class="text-center text-2xl font-medium text-gray-500">Something went wrong</h2>
|
||||||
</div>
|
</div>
|
||||||
@ -120,15 +62,9 @@
|
|||||||
<Frame
|
<Frame
|
||||||
{context}
|
{context}
|
||||||
{fullContext}
|
{fullContext}
|
||||||
sessions={$richSessions.value}
|
sessions={$richSessions}
|
||||||
deltas={derived(
|
deltas={$richSessions?.map((s) => s.deltas)}
|
||||||
$richSessions.value.map(({ deltas }) => deltas),
|
files={$richSessions?.map((s) => s.files)}
|
||||||
(deltas) => deltas
|
|
||||||
)}
|
|
||||||
files={derived(
|
|
||||||
$richSessions.value.map(({ files }) => files),
|
|
||||||
(files) => files
|
|
||||||
)}
|
|
||||||
bind:frame
|
bind:frame
|
||||||
value={$value}
|
value={$value}
|
||||||
/>
|
/>
|
||||||
@ -139,30 +75,14 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<div class="flex-shrink flex-grow"></div>
|
||||||
<div
|
<div
|
||||||
id="controls"
|
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"
|
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"
|
||||||
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);
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
<Slider
|
<Slider sessions={$richSessions?.map(({ deltas }) => deltas)} {bookmarks} bind:value={$value} />
|
||||||
sessions={derived(
|
|
||||||
$richSessions.value.map(({ deltas }) => deltas),
|
|
||||||
(deltas) => deltas
|
|
||||||
)}
|
|
||||||
{bookmarks}
|
|
||||||
bind:value={$value}
|
|
||||||
/>
|
|
||||||
<Playback
|
<Playback
|
||||||
deltas={derived(
|
deltas={$richSessions?.map(({ deltas }) => deltas)}
|
||||||
$richSessions.value.map(({ deltas }) => deltas),
|
|
||||||
(deltas) => deltas
|
|
||||||
)}
|
|
||||||
bind:value={$value}
|
bind:value={$value}
|
||||||
bind:context
|
bind:context
|
||||||
bind:fullContext
|
bind:fullContext
|
||||||
|
@ -3,27 +3,21 @@
|
|||||||
import type { Delta } from '$lib/api/ipc/deltas';
|
import type { Delta } from '$lib/api/ipc/deltas';
|
||||||
import type { Frame } from './frame';
|
import type { Frame } from './frame';
|
||||||
import { DeltasViewer } from '$lib/components';
|
import { DeltasViewer } from '$lib/components';
|
||||||
import type { Readable } from '@square/svelte-store';
|
import type { Loadable, Readable } from '@square/svelte-store';
|
||||||
import { Loaded, type Loadable } from 'svelte-loadable-store';
|
|
||||||
|
|
||||||
export let context: number;
|
export let context: number;
|
||||||
export let fullContext: boolean;
|
export let fullContext: boolean;
|
||||||
export let sessions: Session[];
|
export let sessions: Session[];
|
||||||
export let deltas: Readable<Loadable<[string, Delta][][]>>;
|
export let deltas: [string, Delta][][];
|
||||||
export let files: Readable<Loadable<Partial<Record<string, string>>[]>>;
|
export let files: Partial<Record<string, string>>[];
|
||||||
export let value: number;
|
export let value: number;
|
||||||
export let frame: Frame | null = null;
|
export let frame: Frame | null = null;
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
if (
|
if (deltas && files) {
|
||||||
!$deltas.isLoading &&
|
|
||||||
!$files.isLoading &&
|
|
||||||
Loaded.isValue($deltas) &&
|
|
||||||
Loaded.isValue($files)
|
|
||||||
) {
|
|
||||||
let i = value;
|
let i = value;
|
||||||
for (const j in $deltas.value) {
|
for (const j in deltas) {
|
||||||
const dd = $deltas.value[j];
|
const dd = deltas[j];
|
||||||
if (i < dd.length) {
|
if (i < dd.length) {
|
||||||
const frameDeltas = dd.slice(0, i + 1);
|
const frameDeltas = dd.slice(0, i + 1);
|
||||||
const frameFilepath = frameDeltas[frameDeltas.length - 1][0];
|
const frameFilepath = frameDeltas[frameDeltas.length - 1][0];
|
||||||
@ -34,7 +28,7 @@
|
|||||||
.sort((a, b) => a[1].timestampMs - b[1].timestampMs)
|
.sort((a, b) => a[1].timestampMs - b[1].timestampMs)
|
||||||
.map((delta) => delta[1]),
|
.map((delta) => delta[1]),
|
||||||
filepath: frameFilepath,
|
filepath: frameFilepath,
|
||||||
doc: $files.value[j][frameFilepath] || ''
|
doc: files[j][frameFilepath] || ''
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -45,8 +39,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if frame}
|
{#if frame}
|
||||||
<div id="code" class="flex-auto overflow-auto bg-[#1E2021]">
|
<div id="code" class="overflow-auto">
|
||||||
<div class="pb-[200px]">
|
|
||||||
<DeltasViewer
|
<DeltasViewer
|
||||||
doc={frame.doc}
|
doc={frame.doc}
|
||||||
deltas={frame.deltas}
|
deltas={frame.deltas}
|
||||||
@ -54,7 +47,6 @@
|
|||||||
paddingLines={fullContext ? 100000 : context}
|
paddingLines={fullContext ? 100000 : context}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
{:else}
|
{:else}
|
||||||
<div class="mt-8 text-center">Select a playlist</div>
|
<div class="mt-8 text-center">Select a playlist</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -4,7 +4,6 @@
|
|||||||
import { IconBookmark, IconBookmarkFilled } from '$lib/icons';
|
import { IconBookmark, IconBookmarkFilled } from '$lib/icons';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import { Loaded } from 'svelte-loadable-store';
|
|
||||||
import * as bookmarks from '$lib/api/ipc/bookmarks';
|
import * as bookmarks from '$lib/api/ipc/bookmarks';
|
||||||
import { getBookmark } from '$lib/stores/bookmarks';
|
import { getBookmark } from '$lib/stores/bookmarks';
|
||||||
|
|
||||||
@ -12,12 +11,13 @@
|
|||||||
export let filename: string;
|
export let filename: string;
|
||||||
|
|
||||||
$: bookmark = getBookmark({ projectId: $page.params.projectId, timestampMs });
|
$: bookmark = getBookmark({ projectId: $page.params.projectId, timestampMs });
|
||||||
|
$: bookmarkState = bookmark.state;
|
||||||
|
|
||||||
const toggleBookmark = () => {
|
const toggleBookmark = () => {
|
||||||
if ($bookmark.isLoading) return;
|
if ($bookmarkState?.isLoading) return;
|
||||||
if (Loaded.isError($bookmark)) return;
|
if ($bookmarkState?.isError) return;
|
||||||
bookmarks.upsert(
|
bookmarks.upsert(
|
||||||
!$bookmark.value
|
!$bookmark
|
||||||
? {
|
? {
|
||||||
projectId: $page.params.projectId,
|
projectId: $page.params.projectId,
|
||||||
timestampMs,
|
timestampMs,
|
||||||
@ -25,14 +25,14 @@
|
|||||||
deleted: false
|
deleted: false
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
...$bookmark.value,
|
...$bookmark,
|
||||||
deleted: !$bookmark.value.deleted
|
deleted: !$bookmark.deleted
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if !$bookmark.isLoading && !Loaded.isError($bookmark)}
|
{#if !$bookmarkState?.isLoading && !$bookmarkState?.isError}
|
||||||
<div
|
<div
|
||||||
class="flex max-w-[357px] flex-col gap-2 rounded-[18px] px-4 py-2 shadow"
|
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);
|
style="border: 0.5px solid rgba(63, 63, 70, 0.5);
|
||||||
@ -45,9 +45,9 @@
|
|||||||
{collapse(filename)}
|
{collapse(filename)}
|
||||||
</span>
|
</span>
|
||||||
<button on:click={toggleBookmark} class="z-1">
|
<button on:click={toggleBookmark} class="z-1">
|
||||||
{#if $bookmark.value?.deleted}
|
{#if $bookmark?.deleted}
|
||||||
<IconBookmark class="h-4 w-4 text-zinc-700" />
|
<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" />
|
<IconBookmark class="h-4 w-4 text-zinc-700" />
|
||||||
{:else}
|
{:else}
|
||||||
<IconBookmarkFilled class="h-4 w-4 text-bookmark-selected" />
|
<IconBookmarkFilled class="h-4 w-4 text-bookmark-selected" />
|
||||||
@ -55,7 +55,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{#if $bookmark.value && $bookmark.value.note.length && !$bookmark.value.deleted}
|
{#if $bookmark && $bookmark.note.length && !$bookmark.deleted}
|
||||||
<div
|
<div
|
||||||
role="button"
|
role="button"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@ -64,11 +64,11 @@
|
|||||||
on:keydown={() => events.emit('openBookmarkModal')}
|
on:keydown={() => events.emit('openBookmarkModal')}
|
||||||
>
|
>
|
||||||
<main class="max-h-[7ch] overflow-auto text-text-subdued">
|
<main class="max-h-[7ch] overflow-auto text-text-subdued">
|
||||||
{$bookmark.value.note}
|
{$bookmark.note}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer class="text-right text-sm text-text-subdued">
|
<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>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -5,15 +5,13 @@
|
|||||||
import { unsubscribe } from '$lib/utils';
|
import { unsubscribe } from '$lib/utils';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import * as hotkeys from '$lib/hotkeys';
|
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 value: number;
|
||||||
export let context: number;
|
export let context: number;
|
||||||
export let fullContext: boolean;
|
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 interval: ReturnType<typeof setInterval> | undefined;
|
||||||
let direction: -1 | 1 = 1;
|
let direction: -1 | 1 = 1;
|
||||||
@ -38,9 +36,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const gotoNextDelta = () => {
|
const gotoNextDelta = () => {
|
||||||
if ($maxDeltaIndex.isLoading) return;
|
if (value < maxDeltaIndex) {
|
||||||
if (Loaded.isError($maxDeltaIndex)) return;
|
|
||||||
if (value < $maxDeltaIndex.value) {
|
|
||||||
value += 1;
|
value += 1;
|
||||||
} else {
|
} else {
|
||||||
stop();
|
stop();
|
||||||
@ -102,7 +98,7 @@
|
|||||||
<div class="back-forward-button-container">
|
<div class="back-forward-button-container">
|
||||||
<button
|
<button
|
||||||
on:click={gotoPrevDelta}
|
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
|
<svg
|
||||||
width="20"
|
width="20"
|
||||||
@ -116,15 +112,14 @@
|
|||||||
fill-rule="evenodd"
|
fill-rule="evenodd"
|
||||||
clip-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"
|
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"
|
fill="currentColor"
|
||||||
class="fill-zinc-400 group-hover:fill-zinc-100"
|
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
on:click={gotoNextDelta}
|
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
|
<svg
|
||||||
width="20"
|
width="20"
|
||||||
@ -138,8 +133,7 @@
|
|||||||
fill-rule="evenodd"
|
fill-rule="evenodd"
|
||||||
clip-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"
|
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"
|
fill="currentColor"
|
||||||
class="fill-zinc-400 group-hover:fill-zinc-100"
|
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
@ -1,20 +1,19 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Delta } from '$lib/api/ipc/deltas';
|
import type { Delta } from '$lib/api/ipc/deltas';
|
||||||
import type { Bookmark } from '$lib/api/ipc/bookmarks';
|
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 { ModuleChapters, ModuleMarkers, type Marker } from './slider';
|
||||||
import { JSR, ModuleSlider } from 'mm-jsr';
|
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 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)
|
bookmarks.filter(({ deleted }) => !deleted).map((bookmark) => bookmark.timestampMs)
|
||||||
);
|
);
|
||||||
|
|
||||||
$: markers = derived([sessions, bookmarkedTimestamps], ([sessions, bookmarkedTimestamps]) =>
|
$: markers = asyncDerived([bookmarkedTimestamps], async ([bookmarkedTimestamps]) =>
|
||||||
sessions.flatMap((session, index, all) => {
|
sessions.flatMap((session, index, all) => {
|
||||||
const from = all.slice(0, index).reduce((acc, deltas) => acc + deltas.length, 0);
|
const from = all.slice(0, index).reduce((acc, deltas) => acc + deltas.length, 0);
|
||||||
return session
|
return session
|
||||||
@ -27,17 +26,13 @@
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
$: totalDeltas = derived(sessions, (sessions) =>
|
$: totalDeltas = sessions?.reduce((acc, deltas) => acc + deltas.length, 0);
|
||||||
sessions.reduce((acc, deltas) => acc + deltas.length, 0)
|
|
||||||
);
|
|
||||||
|
|
||||||
$: chapters = derived(sessions, (sessions) =>
|
$: chapters = sessions?.map((session, index, all) => {
|
||||||
sessions.map((session, index, all) => {
|
|
||||||
const from = all.slice(0, index).reduce((acc, deltas) => acc + deltas.length, 0);
|
const from = all.slice(0, index).reduce((acc, deltas) => acc + deltas.length, 0);
|
||||||
const to = from + session.length;
|
const to = from + session.length;
|
||||||
return [from, to] as [number, number];
|
return [from, to] as [number, number];
|
||||||
})
|
});
|
||||||
);
|
|
||||||
|
|
||||||
type Config = {
|
type Config = {
|
||||||
min: number;
|
min: number;
|
||||||
@ -90,14 +85,15 @@
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if !$totalDeltas.isLoading && Loaded.isValue($totalDeltas) && !$chapters.isLoading && Loaded.isValue($chapters) && !$markers.isLoading && Loaded.isValue($markers)}
|
{#if totalDeltas && chapters && $markers}
|
||||||
<div
|
<div
|
||||||
|
class="bg-color-1 rounded"
|
||||||
use:jsrSlider={{
|
use:jsrSlider={{
|
||||||
min: 0,
|
min: 0,
|
||||||
max: $totalDeltas.value,
|
max: totalDeltas,
|
||||||
initialValue: value,
|
initialValue: value,
|
||||||
chapters: $chapters.value,
|
chapters: chapters,
|
||||||
markers: $markers.value
|
markers: $markers
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<style>
|
<style>
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
import { Button, Link } from '$lib/components';
|
import { Button, Link } from '$lib/components';
|
||||||
import { BranchController } from '$lib/vbranches/branchController';
|
import { BranchController } from '$lib/vbranches/branchController';
|
||||||
import { getContext } from 'svelte';
|
import { getContext, onMount } from 'svelte';
|
||||||
import { SETTINGS_CONTEXT, type SettingsStore } from '$lib/userSettings';
|
import { SETTINGS_CONTEXT, type SettingsStore } from '$lib/userSettings';
|
||||||
import { IconExternalLink } from '$lib/icons';
|
import { IconExternalLink } from '$lib/icons';
|
||||||
import {
|
import {
|
||||||
@ -24,6 +24,8 @@
|
|||||||
import IconChevronLeft from '$lib/icons/IconChevronLeft.svelte';
|
import IconChevronLeft from '$lib/icons/IconChevronLeft.svelte';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import BaseBranchSelect from './BaseBranchSelect.svelte';
|
import BaseBranchSelect from './BaseBranchSelect.svelte';
|
||||||
|
import { unsubscribe } from '$lib/utils';
|
||||||
|
import * as hotkeys from '$lib/hotkeys';
|
||||||
|
|
||||||
export let data: PageData;
|
export let data: PageData;
|
||||||
let { projectId, project, cloud } = data;
|
let { projectId, project, cloud } = data;
|
||||||
@ -31,7 +33,7 @@
|
|||||||
const userSettings = getContext<SettingsStore>(SETTINGS_CONTEXT);
|
const userSettings = getContext<SettingsStore>(SETTINGS_CONTEXT);
|
||||||
|
|
||||||
const fetchStore = getFetchesStore(projectId);
|
const fetchStore = getFetchesStore(projectId);
|
||||||
const deltasStore = getDeltasStore(projectId);
|
const deltasStore = getDeltasStore(projectId, undefined, true);
|
||||||
const headStore = getHeadsStore(projectId);
|
const headStore = getHeadsStore(projectId);
|
||||||
const sessionsStore = getSessionStore(projectId);
|
const sessionsStore = getSessionStore(projectId);
|
||||||
const baseBranchStore = getBaseBranchStore(projectId, fetchStore, headStore);
|
const baseBranchStore = getBaseBranchStore(projectId, fetchStore, headStore);
|
||||||
@ -71,6 +73,14 @@
|
|||||||
function updateDeltasStore(sid: string | undefined) {
|
function updateDeltasStore(sid: string | undefined) {
|
||||||
if (sid) deltasStore.setSessionId(sid);
|
if (sid) deltasStore.setSessionId(sid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMount(() =>
|
||||||
|
unsubscribe(
|
||||||
|
hotkeys.on('Meta+Shift+R', () =>
|
||||||
|
goto(location.href.replace('/repo/', '/projects/') + '/player')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $baseBranchesState.isLoading}
|
{#if $baseBranchesState.isLoading}
|
||||||
|
@ -35,8 +35,6 @@
|
|||||||
export let selectable = false;
|
export let selectable = false;
|
||||||
export let selectedOwnership: Writable<Ownership>;
|
export let selectedOwnership: Writable<Ownership>;
|
||||||
|
|
||||||
$: console.log(file)
|
|
||||||
|
|
||||||
const userSettings = getContext<SettingsStore>(SETTINGS_CONTEXT);
|
const userSettings = getContext<SettingsStore>(SETTINGS_CONTEXT);
|
||||||
const dispatch = createEventDispatcher<{
|
const dispatch = createEventDispatcher<{
|
||||||
expanded: boolean;
|
expanded: boolean;
|
||||||
|
Loading…
Reference in New Issue
Block a user