mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-11-26 12:24:26 +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
|
||||
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}
|
||||
|
@ -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 }
|
||||
);
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
<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}
|
||||
/>
|
||||
|
@ -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
|
||||
};
|
||||
};
|
||||
|
@ -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">
|
||||
|
@ -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.
|
||||
|
@ -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>
|
||||
|
@ -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)
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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
|
||||
};
|
||||
};
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user