remove old commit page

This commit is contained in:
Nikita Galaiko 2023-09-20 12:33:03 +02:00
parent 92472e5e2c
commit 5c98330cc0
10 changed files with 1 additions and 887 deletions

View File

@ -440,35 +440,6 @@ impl App {
gb_repository.push()
}
pub fn git_stage_files<P: AsRef<std::path::Path>>(
&self,
project_id: &str,
paths: Vec<P>,
) -> Result<()> {
let project = self.gb_project(project_id)?;
let project_repository = project_repository::Repository::open(&project)
.context("failed to open project repository")?;
project_repository.git_stage_files(paths)
}
pub fn git_unstage_files<P: AsRef<std::path::Path>>(
&self,
project_id: &str,
paths: Vec<P>,
) -> Result<()> {
let project = self.gb_project(project_id)?;
let project_repository = project_repository::Repository::open(&project)
.context("failed to open project repository")?;
project_repository.git_unstage_files(paths)
}
pub fn git_commit(&self, project_id: &str, message: &str) -> Result<()> {
let project = self.gb_project(project_id)?;
let project_repository = project_repository::Repository::open(&project)
.context("failed to open project repository")?;
project_repository.git_commit(message)
}
pub fn search(&self, query: &search::Query) -> Result<search::Results> {
self.searcher.search(query)
}

View File

@ -389,45 +389,6 @@ async fn git_head(handle: tauri::AppHandle, project_id: &str) -> Result<String,
Ok(head)
}
#[tauri::command(async)]
#[instrument(skip(handle))]
async fn git_stage(
handle: tauri::AppHandle,
project_id: &str,
paths: Vec<&str>,
) -> Result<(), Error> {
let app = handle.state::<app::App>();
app.git_stage_files(project_id, paths)
.with_context(|| format!("failed to stage file for project {}", project_id))?;
Ok(())
}
#[tauri::command(async)]
#[instrument(skip(handle))]
async fn git_unstage(
handle: tauri::AppHandle,
project_id: &str,
paths: Vec<&str>,
) -> Result<(), Error> {
let app = handle.state::<app::App>();
app.git_unstage_files(project_id, paths)
.with_context(|| format!("failed to unstage file for project {}", project_id))?;
Ok(())
}
#[tauri::command(async)]
#[instrument(skip(handle))]
async fn git_commit(
handle: tauri::AppHandle,
project_id: &str,
message: &str,
) -> Result<(), Error> {
let app = handle.state::<app::App>();
app.git_commit(project_id, message)
.with_context(|| format!("failed to commit for project {}", project_id))?;
Ok(())
}
#[tauri::command(async)]
#[instrument(skip(handle))]
async fn delete_all_data(handle: tauri::AppHandle) -> Result<(), Error> {
@ -660,9 +621,6 @@ async fn main() {
git_remote_branches,
git_remote_branches_data,
git_head,
git_commit,
git_stage,
git_unstage,
git_wd_diff,
delete_all_data,
get_logs_archive_path,

View File

@ -328,52 +328,6 @@ impl<'repository> Repository<'repository> {
Ok(oids.len().try_into()?)
}
pub fn git_stage_files<P: AsRef<std::path::Path>>(&self, paths: Vec<P>) -> Result<()> {
let mut index = self.git_repository.index()?;
for path in paths {
let path = path.as_ref();
// to "stage" a file means to:
// - remove it from the index if file is deleted
// - overwrite it in the index otherwise
if !std::path::Path::new(&self.project.path).join(path).exists() {
index.remove_path(path).with_context(|| {
format!("failed to remove path {} from index", path.display())
})?;
} else {
index
.add_path(path)
.with_context(|| format!("failed to add path {} to index", path.display()))?;
}
}
index.write().with_context(|| "failed to write index")?;
Ok(())
}
pub fn git_unstage_files<P: AsRef<std::path::Path>>(&self, paths: Vec<P>) -> Result<()> {
let head_tree = self.git_repository.head()?.peel_to_tree()?;
let mut head_index = git::Index::new()?;
head_index.read_tree(&head_tree)?;
let mut index = self.git_repository.index()?;
for path in paths {
let path = path.as_ref();
// to "unstage" a file means to:
// - put head version of the file in the index if it exists
// - remove it from the index otherwise
let head_index_entry = head_index.get_path(path, 0);
if let Some(entry) = head_index_entry {
index
.add(&entry)
.with_context(|| format!("failed to add path {} to index", path.display()))?;
} else {
index.remove_path(path).with_context(|| {
format!("failed to remove path {} from index", path.display())
})?;
}
}
index.write().with_context(|| "failed to write index")?;
Ok(())
}
// returns a remote and makes sure that the push url is an ssh url
// if url is already ssh, or not set at all, then it returns the remote as is.
fn get_remote(&'repository self, name: &str) -> Result<git::Remote<'repository>> {

View File

@ -2,18 +2,6 @@ export type { Activity } from './activities';
import { invoke } from '$lib/ipc';
export function commit(params: { projectId: string; message: string }) {
return invoke<boolean>('git_commit', params);
}
export function stage(params: { projectId: string; paths: Array<string> }) {
return invoke<void>('git_stage', params);
}
export function unstage(params: { projectId: string; paths: Array<string> }) {
return invoke<void>('git_unstage', params);
}
export function matchFiles(params: { projectId: string; matchPattern: string }) {
return invoke<string[]>('git_match_paths', params);
}

View File

@ -2,7 +2,6 @@ import type { Project } from '$lib/api/ipc/projects';
import { matchFiles } from '$lib/api/git';
import * as events from '$lib/events';
import {
IconGitCommit,
IconFile,
IconFeedback,
IconSettings,
@ -106,12 +105,6 @@ const commandsGroup = ({ project, input }: { project?: Project; input: string })
: []),
...(project
? [
{
title: 'Quick commits...',
hotkey: 'C',
action: () => events.emit('openQuickCommitModal'),
icon: IconGitCommit
},
{
title: 'Replay',
hotkey: 'Meta+R',
@ -138,14 +131,6 @@ const navigateGroup = ({ project, input }: { project?: Project; input: string })
commands: [
...(project
? [
{
title: 'Commits',
hotkey: 'Meta+Shift+C',
action: {
href: `/projects/${project.id}/commit/`
},
icon: IconGitCommit
},
{
title: 'Project settings',
hotkey: 'Meta+Shift+,',

View File

@ -5,7 +5,6 @@ type Events = {
openCommandPalette: () => void;
closeCommandPalette: () => void;
openNewProjectModal: () => void;
openQuickCommitModal: () => void;
openSendIssueModal: () => void;
openBookmarkModal: () => void;
createBookmark: (params: { projectId: string }) => void;

View File

@ -4,8 +4,7 @@
import { IconGitBranch } from '$lib/icons';
import { derived } from '@square/svelte-store';
import FileSummaries from './FileSummaries.svelte';
import { Button, Statuses, Tooltip } from '$lib/components';
import { goto } from '$app/navigation';
import { Statuses, Tooltip } from '$lib/components';
import Chat from './Chat.svelte';
export let data: PageData;
@ -44,17 +43,6 @@
</span>
</div>
</Tooltip>
{#await statuses.load()}
<Button disabled color="purple">Commit changes</Button>
{:then}
<Button
disabled={Object.keys($statuses).length === 0}
color="purple"
on:click={() => goto(`/projects/${$project?.id}/commit`)}
>
Commit changes
</Button>
{/await}
</div>
{#await statuses.load() then}
<Statuses statuses={$statuses} />

View File

@ -1,212 +0,0 @@
<script lang="ts">
import * as toasts from '$lib/toasts';
import type { Project } from '$lib/api/ipc/projects';
import type { User, getCloudApiClient } from '$lib/api/cloud/api';
import { isUnstaged, type Status } from '$lib/api/git/statuses';
import { commit, stage } from '$lib/api/git';
import { Button, Link } from '$lib/components';
import { IconGitBranch, IconSparkle } from '$lib/icons';
import { Stats } from '$lib/components';
import Overlay from '$lib/components/Overlay/Overlay.svelte';
export function show() {
modal.show();
}
export let project: Project;
export let head: string;
export let statuses: Record<string, Status>;
export let diffs: Record<string, string>;
export let user: User;
export let cloud: ReturnType<typeof getCloudApiClient>;
let summary = '';
let description = '';
let isAutowriting = false;
let isCommitting = false;
const stageAll = async () => {
const paths = Object.entries(statuses)
.filter((entry) => isUnstaged(entry[1]))
.map(([path]) => path);
if (paths.length === 0) return;
await stage({
projectId: project.id,
paths
});
};
$: [linesAdded, linesRemoved] = Object.values(diffs)
.map((diff) => {
let added = 0;
let removed = 0;
let isHeader = true;
for (const line of diff.split('\n')) {
if (isHeader) {
if (line.startsWith('@@')) {
isHeader = false;
}
continue;
} else if (line.startsWith('+')) {
added++;
} else if (line.startsWith('-')) {
removed++;
}
}
return [added, removed];
})
.reduce((a, b) => [a[0] + b[0], a[1] + b[1]], [0, 0]);
const reset = () => {
summary = '';
description = '';
};
const onCommit = async (e: SubmitEvent) => {
const form = e.target as HTMLFormElement;
const formData = new FormData(form);
const summary = formData.get('commit-message') as string;
const description = formData.get('commit-description') as string;
isCommitting = true;
await stageAll();
commit({
projectId: project.id,
message: description.length > 0 ? `${summary}\n\n${description}` : summary
})
.then(() => {
toasts.success('Commit created');
reset();
})
.catch(() => {
toasts.error('Failed to commit');
})
.finally(() => {
isCommitting = false;
modal.close();
});
};
const onAutowrite = async () => {
const diff = Object.values(diffs).join('\n').slice(0, 5000);
const backupSummary = summary;
const backupDescription = description;
summary = '';
description = '';
isAutowriting = true;
cloud.summarize
.commit(user.access_token, {
diff,
uid: project.id
})
.then(({ message }) => {
const firstNewLine = message.indexOf('\n');
summary = firstNewLine > -1 ? message.slice(0, firstNewLine).trim() : message;
description = firstNewLine > -1 ? message.slice(firstNewLine + 1).trim() : '';
})
.catch(() => {
summary = backupSummary;
description = backupDescription;
toasts.error('Failed to generate commit message');
})
.finally(() => {
isAutowriting = false;
});
};
let modal: Overlay;
</script>
<Overlay bind:this={modal} let:close>
<form
class="modal modal-quick-commit font-modal-stroke/50 flex w-[680px] flex-col"
on:submit|preventDefault={onCommit}
>
<header class="flex w-full items-center justify-between p-4">
<h2 class="flex items-center gap-2">
<IconGitBranch class="h-5 w-5 text-zinc-400" />
<span class="line-height-5 text-zinc-300">{head}</span>
</h2>
</header>
<div class="flex flex-col px-4">
<!-- svelte-ignore a11y-autofocus -->
<div class="flex w-full items-center justify-between gap-2">
<input
autocomplete="off"
autocorrect="off"
spellcheck="true"
autofocus
name="commit-message"
contenteditable="true"
class="quick-commit-input break-word outline-none-important w-full overflow-auto border-0 border-none bg-transparent p-1 text-xl text-zinc-100"
type="text"
placeholder="Commit message (required)"
disabled={isAutowriting || isCommitting}
bind:value={summary}
required
/>
<Button
color="purple"
kind="outlined"
disabled={isCommitting || !project.api?.sync}
loading={isAutowriting}
on:click={onAutowrite}
>
<IconSparkle class="h-5 w-5" />
</Button>
</div>
<textarea
autocomplete="off"
autocorrect="off"
spellcheck="true"
bind:value={description}
name="commit-description"
class="quick-commit-input outline-none-important resize-none border-none bg-transparent p-1 text-lg text-zinc-400"
placeholder="Commit description (optional)"
disabled={isAutowriting || isCommitting}
rows="6"
/>
</div>
<footer class="flex items-center justify-between p-4">
<div class="flex items-center gap-4">
<Link
on:click={modal?.close}
disabled={isAutowriting || isCommitting}
href="/projects/{project.id}/commit/"
>
{Object.keys(statuses).length} files changed
<Stats added={linesAdded} removed={linesRemoved} />
</Link>
</div>
<div class="flex gap-2">
<Button kind="outlined" on:click={close}>Cancel</Button>
<Button type="submit" disabled={isAutowriting} color="purple" loading={isCommitting}>
Commit
</Button>
</div>
</footer>
</form>
</Overlay>
<style lang="postcss">
.quick-commit-input {
@apply outline-none focus:outline-none active:outline-none;
outline: none;
}
.quick-commit-input:focus {
outline: 0;
outline-offset: 0;
box-shadow: rgb(255, 255, 255) 0px 0px 0px 0px, rgba(37, 99, 235, 0) 0px 0px 0px 2px,
rgba(0, 0, 0, 0) 0px 0px 0px 0px;
}
footer {
box-shadow: inset 0px 1px 0px rgba(0, 0, 0, 0.1);
}
</style>

View File

@ -1,505 +0,0 @@
<script lang="ts">
import type { PageData } from './$types';
import { Button, Checkbox, DiffContext } from '$lib/components';
import { collapse } from '$lib/paths';
import { derived, writable } from '@square/svelte-store';
import { isStaged, isUnstaged } from '$lib/api/git/statuses';
import { userStore } from '$lib/stores/user';
import { commit, stage, unstage } from '$lib/api/git';
import DiffViewer from './DiffViewer.svelte';
import { page } from '$app/stores';
import { error, success } from '$lib/toasts';
import { fly } from 'svelte/transition';
import { Modal } from '$lib/components';
import * as hotkeys from '$lib/hotkeys';
import { IconChevronDown, IconChevronUp } from '$lib/icons';
import { onMount } from 'svelte';
import { unsubscribe } from '$lib/utils';
export let data: PageData;
let { statuses, diffs, cloud, project } = data;
const user = userStore;
let fullContext = false;
let context = 3;
const stagedFiles = derived(statuses, (statuses) =>
Object.entries(statuses ?? {})
.filter((status) => isStaged(status[1]))
.map(([path]) => path)
);
const unstagedFiles = derived(statuses, (statuses) =>
Object.entries(statuses ?? {})
.filter((status) => isUnstaged(status[1]))
.map(([path]) => path)
);
const allFiles = derived(statuses, (statuses) =>
Object.keys(statuses ?? {}).sort((a, b) => a.localeCompare(b))
);
let connectToCloudModal: Modal;
let summary = '';
let description = '';
const selectedDiffPath = writable<string | undefined>(
Object.keys($statuses ?? {})
.sort((a, b) => a.localeCompare(b))
.at(0)
);
statuses.subscribe((statuses) => {
if ($selectedDiffPath && Object.keys(statuses ?? {}).includes($selectedDiffPath)) return;
$selectedDiffPath = Object.keys(statuses ?? {})
.sort((a, b) => a.localeCompare(b))
.at(0);
});
const selectedDiff = derived([diffs, selectedDiffPath], ([diffs, selectedDiffPath]) =>
diffs && selectedDiffPath ? diffs[selectedDiffPath] : undefined
);
const nextFilePath = derived([allFiles, selectedDiffPath], ([files, selectedDiffPath]) => {
if (selectedDiffPath === undefined) return;
const index = files.indexOf(selectedDiffPath);
if (index === files.length - 1) return;
return files[index + 1];
});
const previousFilePath = derived([allFiles, selectedDiffPath], ([files, selectedDiffPath]) => {
if (selectedDiffPath === undefined) return;
const index = files.indexOf(selectedDiffPath);
if (index === 0) return;
return files[index - 1];
});
const selectNextFile = () => {
if ($nextFilePath) $selectedDiffPath = $nextFilePath;
};
const selectPreviousFile = () => {
if ($previousFilePath) $selectedDiffPath = $previousFilePath;
};
const hasNextFile = derived(nextFilePath, (nextFilePath) => nextFilePath !== undefined);
const hasPreviousFile = derived(
previousFilePath,
(previousFilePath) => previousFilePath !== undefined
);
const reset = () => {
summary = '';
description = '';
selectedDiffPath.set(undefined);
};
let isCommitting = false;
let isGeneratingCommitMessage = false;
const onCommit = async (e: SubmitEvent) => {
const form = e.target as HTMLFormElement;
const formData = new FormData(form);
const summary = formData.get('summary') as string;
const description = formData.get('description') as string;
isCommitting = true;
commit({
projectId: $page.params.projectId,
message: description.length > 0 ? `${summary}\n\n${description}` : summary
})
.then(() => {
success('Commit created');
reset();
})
.catch(() => {
error('Failed to commit');
})
.finally(() => {
isCommitting = false;
});
};
const onGenerateCommitMessage = async () => {
if (!isCloudEnabled) {
connectToCloudModal.show();
return;
}
if ($user === null) return;
const partialDiff = Object.fromEntries(
Object.entries($diffs ?? {}).filter(([key]) => $statuses[key] && isStaged($statuses[key]))
);
const diff = Object.values(partialDiff).join('\n').slice(0, 5000);
const backupSummary = summary;
const backupDescription = description;
summary = '';
description = '';
isGeneratingCommitMessage = true;
cloud.summarize
.commit($user.access_token, {
diff,
uid: $page.params.projectId
})
.then(({ message }) => {
const firstNewLine = message.indexOf('\n');
summary = firstNewLine > -1 ? message.slice(0, firstNewLine).trim() : message;
description = firstNewLine > -1 ? message.slice(firstNewLine + 1).trim() : '';
})
.catch(() => {
summary = backupSummary;
description = backupDescription;
error('Failed to generate commit message');
})
.finally(() => {
isGeneratingCommitMessage = false;
});
};
const onGroupCheckboxClick = (e: Event) => {
const target = e.target as HTMLInputElement;
if (target.checked) {
stage({
projectId: $page.params.projectId,
paths: $unstagedFiles
}).catch(() => {
error('Failed to stage files');
});
} else {
unstage({
projectId: $page.params.projectId,
paths: $stagedFiles
}).catch(() => {
error('Failed to unstage files');
});
}
};
const enableProjectSync = async () => {
if ($project === undefined) return;
if ($user === null) return;
try {
if (!$project.api) {
const apiProject = await cloud.projects.create($user.access_token, {
name: $project.title,
uid: $project.id
});
await project.update({ api: { ...apiProject, sync: true } });
} else {
await project.update({ api: { ...$project.api, sync: true } });
}
} catch (e) {
console.error(`Failed to update project sync status: ${e}`);
error('Failed to update project sync status');
}
};
$: isCommitEnabled = summary.length > 0 && $stagedFiles.length > 0;
$: isLoggedIn = $user !== null;
$: isCloudEnabled = $project?.api?.sync;
$: isSomeFilesSelected = $stagedFiles.length > 0 && $allFiles.length > 0;
$: isGenerateCommitEnabled = isLoggedIn && isSomeFilesSelected;
// a situation where a file is created, then added to git index, and then deleted
// is not handled by our UI very good. to simplify things, we just stage the file
// which effectively removes it from the UI and keeps consistency between our ui
// an git
statuses.subscribe((statuses) =>
Object.entries(statuses ?? {}).forEach(([file, status]) => {
const isStagedAdded = isStaged(status) && status.staged === 'added';
const isUnstagedDeleted = isUnstaged(status) && status.unstaged === 'deleted';
if (isStagedAdded && isUnstagedDeleted)
stage({ projectId: $page.params.projectId, paths: [file] });
})
);
onMount(() =>
unsubscribe(
hotkeys.on('ArrowUp', () => selectPreviousFile()),
hotkeys.on('Control+n', () => selectPreviousFile()),
hotkeys.on('k', () => selectPreviousFile()),
hotkeys.on('ArrowDown', () => selectNextFile()),
hotkeys.on('Control+p', () => selectNextFile()),
hotkeys.on('j', () => selectNextFile())
)
);
</script>
<Modal bind:this={connectToCloudModal} title="GitButler Cloud required">
<div class="flex flex-col gap-2">
<p>
By connecting to GitButler Cloud you'll unlock improved, cloud only features, including
AI-generated commit summaries, and the assurance of never losing your work with synced
project.
</p>
<span class="font-semibold text-zinc-300">AI-genearate commits</span>
<p class="flex flex-col">
This not only saves you time and effort but also ensures consistency in tone and style,
ultimately helping you to boost sales and improve customer satisfaction.
</p>
<span class="font-semibold text-zinc-300">Secure and reliable backup</span>
<p>
GitButler backup guarantees that anything youve ever written in your projects are safe,
secure and easily recoverable.
</p>
</div>
<svelte:fragment slot="controls" let:close>
<Button kind="outlined" on:click={close}>Cancel</Button>
<Button color="purple" on:click={() => enableProjectSync().finally(close)}>Connect</Button>
</svelte:fragment>
</Modal>
<div id="commit-page" class="flex h-full w-full">
<div class="commit-panel-container side-panel flex flex-col">
<form on:submit|preventDefault={onCommit} class="flex h-full flex-col gap-4 px-4">
<h1 class="pt-2 text-2xl font-bold">Commit</h1>
<ul class="card flex h-full w-full flex-col overflow-auto">
<header class="flex w-full items-center rounded-tl rounded-tr bg-card-active p-2">
{#await Promise.all([stagedFiles.load(), unstagedFiles.load(), allFiles.load()]) then}
<Checkbox
checked={$allFiles.length > 0 && $stagedFiles.length === $allFiles.length}
indeterminate={$stagedFiles.length > 0 &&
$unstagedFiles.length > 0 &&
$allFiles.length > 0}
disabled={isCommitting || isGeneratingCommitMessage}
on:click={onGroupCheckboxClick}
/>
<h1 class="m-auto flex">
<span class="w-full text-center">{$allFiles.length} changed files</span>
</h1>
{/await}
</header>
<div class="changed-file-list-container h-100 overflow-y-auto">
{#await Promise.all([statuses.load(), selectedDiffPath.load()]) then}
{#each Object.entries($statuses).sort( (a, b) => a[0].localeCompare(b[0]) ) as [path, status]}
<li class="bg-card-default last:mb-1">
<div
class:bg-[#3356C2]={$selectedDiffPath === path}
class:hover:bg-divider={$selectedDiffPath !== path}
class="file-changed-item mx-1 mt-1 flex select-text items-center gap-2 rounded bg-card-default px-1 py-1"
>
<Checkbox
checked={isStaged(status)}
name="path"
disabled={isCommitting || isGeneratingCommitMessage}
value={path}
on:click={() => {
isStaged(status)
? unstage({ projectId: $page.params.projectId, paths: [path] }).catch(
() => {
error('Failed to unstage file');
}
)
: stage({ projectId: $page.params.projectId, paths: [path] }).catch(() => {
error('Failed to stage file');
});
}}
/>
<label class="flex h-5 w-full overflow-auto" for="path">
<button
disabled={isCommitting || isGeneratingCommitMessage}
on:click|preventDefault={() => ($selectedDiffPath = path)}
type="button"
class="h-full w-full select-auto text-left font-mono text-base disabled:opacity-50"
>
{collapse(path)}
</button>
</label>
</div>
</li>
{/each}
{/await}
</div>
</ul>
<div class="bottom-controller-container flex flex-col gap-2 pb-4">
<input
autocomplete="off"
autocorrect="off"
spellcheck="true"
name="summary"
class="w-full"
disabled={isGeneratingCommitMessage || isCommitting}
type="text"
placeholder="Summary (required)"
bind:value={summary}
required
/>
<div class="commit-description-container relative h-36">
{#if isGeneratingCommitMessage}
<div
in:fly={{ y: 8, duration: 500 }}
out:fly={{ y: -8, duration: 500 }}
class="generating-commit absolute bottom-0 left-0 right-0 top-0 rounded border-2 border-[#502E5C]"
>
<div
class="generating-commit-message absolute bottom-0 left-0 rounded-tr bg-[#782E94] bg-gradient-to-b from-[#623871] to-[#502E5C] px-2 py-1"
>
<span>✨ Summarizing changes</span>
<span class="dot-container">
<div class="dot" />
<div class="dot" />
<div class="dot" />
</span>
</div>
</div>
{/if}
<textarea
autocomplete="off"
autocorrect="off"
spellcheck="true"
name="description"
disabled={isGeneratingCommitMessage || isCommitting}
class="h-full w-full resize-none"
rows="10"
placeholder="Description (optional)"
bind:value={description}
/>
</div>
<div class="flex justify-between">
<Button
color="purple"
disabled={!isGenerateCommitEnabled}
on:click={onGenerateCommitMessage}
loading={isGeneratingCommitMessage}
>
✨ Autowrite
</Button>
<Button
loading={isCommitting}
disabled={!isCommitEnabled || isGeneratingCommitMessage}
color="purple"
type="submit"
>
Commit changes
</Button>
</div>
</div>
</form>
</div>
<div class="main-content-container">
<div id="preview" class="card relative m-2 flex h-full flex-col overflow-auto">
{#await Promise.all([selectedDiffPath.load(), selectedDiff.load()])}
<div class="flex h-full w-full flex-col items-center justify-center">
<p class="text-lg">Loading...</p>
</div>
{:then}
{#if !$selectedDiffPath}
<div class="flex h-full w-full flex-col items-center justify-center">
<p class="text-lg">Select a file to preview changes</p>
</div>
{:else if !$selectedDiff}
<div class="flex h-full w-full flex-col items-center justify-center">
<p class="text-lg">Unable to load diff</p>
</div>
{:else}
<header class="flex items-center gap-3 bg-card-active py-2 pl-2 pr-3">
<div class="flex items-center gap-1">
<button
on:click={selectPreviousFile}
class="rounded border border-zinc-500 bg-zinc-600 p-0.5"
class:hover:bg-zinc-500={$hasPreviousFile}
class:cursor-not-allowed={!$hasPreviousFile}
class:text-zinc-500={!$hasPreviousFile}
>
<IconChevronUp class="h-4 w-4" />
</button>
<button
on:click={selectNextFile}
class="rounded border border-zinc-500 bg-zinc-600 p-0.5"
class:hover:bg-zinc-500={$hasNextFile}
class:cursor-not-allowed={!$hasNextFile}
class:text-zinc-500={!$hasNextFile}
>
<IconChevronDown class="h-4 w-4" />
</button>
</div>
<span>{$selectedDiffPath}</span>
</header>
<div id="code" class="flex-auto overflow-auto bg-[#1E2021]">
<div class="pb-[65px]">
<DiffViewer
diff={$selectedDiff ?? ''}
path={$selectedDiffPath}
paddingLines={fullContext ? 10000 : context}
/>
</div>
</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);
"
>
<DiffContext bind:lines={context} bind:fullContext />
</div>
{/if}
{/await}
</div>
</div>
</div>
<style lang="postcss">
.changed-file-list-container {
max-height: calc(100vh - 200px);
}
/**
* ==============================================
* Dot Typing
* ==============================================
*/
.dot-container {
padding-left: 4px;
padding-bottom: 3px;
}
.dot {
@apply bg-zinc-200;
display: inline-block;
width: 3px;
height: 3px;
border-radius: 50%;
position: relative;
bottom: 3px;
}
.dot-container .dot:nth-last-child(1) {
animation: jumpingAnimation 1.2s 0.6s linear infinite;
}
.dot-container .dot:nth-last-child(2) {
animation: jumpingAnimation 1.2s 0.3s linear infinite;
}
.dot-container .dot:nth-last-child(3) {
animation: jumpingAnimation 1.2s 0s linear infinite;
}
@keyframes jumpingAnimation {
0% {
transform: translate(0, 0);
}
16% {
transform: translate(0, -5px);
}
33% {
transform: translate(0, 0);
}
100% {
transform: translate(0, 0);
}
}
</style>

View File

@ -1,12 +0,0 @@
<script lang="ts">
import { parse } from '$lib/diff';
import { Differ } from '$lib/components';
export let diff: string;
export let path: string;
export let paddingLines = 3;
$: parsedDiff = parse(diff);
</script>
<Differ diff={parsedDiff} filepath={path} {paddingLines} />