mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-11-24 05:29:51 +03:00
project page
This commit is contained in:
parent
bf422c027a
commit
76d3b31054
@ -16,6 +16,7 @@
|
||||
"diff": "^5.1.0",
|
||||
"idb-keyval": "^6.2.0",
|
||||
"mm-jsr": "^3.0.2",
|
||||
"nanoid": "^4.0.0",
|
||||
"tauri-plugin-fs-watch-api": "github:tauri-apps/tauri-plugin-fs-watch",
|
||||
"yjs": "^13.5.45"
|
||||
},
|
||||
|
@ -10,6 +10,7 @@ specifiers:
|
||||
diff: ^5.1.0
|
||||
idb-keyval: ^6.2.0
|
||||
mm-jsr: ^3.0.2
|
||||
nanoid: ^4.0.0
|
||||
postcss: ^8.4.14
|
||||
postcss-load-config: ^4.0.1
|
||||
svelte: ^3.54.0
|
||||
@ -27,6 +28,7 @@ dependencies:
|
||||
diff: 5.1.0
|
||||
idb-keyval: 6.2.0
|
||||
mm-jsr: 3.0.2
|
||||
nanoid: 4.0.0
|
||||
tauri-plugin-fs-watch-api: github.com/tauri-apps/tauri-plugin-fs-watch/7d63ad9dfd72ae6c2bb05c148f344adb0521ec3a
|
||||
yjs: 13.5.45
|
||||
|
||||
@ -948,6 +950,12 @@ packages:
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/nanoid/4.0.0:
|
||||
resolution: {integrity: sha512-IgBP8piMxe/gf73RTQx7hmnhwz0aaEXYakvqZyE302IXW3HyVNhdNGC+O2MwMAVhLEnvXlvKtGbtJf6wvHihCg==}
|
||||
engines: {node: ^14 || ^16 || >=18}
|
||||
hasBin: true
|
||||
dev: false
|
||||
|
||||
/node-releases/2.0.9:
|
||||
resolution: {integrity: sha512-2xfmOrRkGogbTK9R6Leda0DGiXeY3p2NJpy4+gNCffdUvV6mdEJnaDEic1i3Ec2djAo8jWYoJMR5PB0MSMpxUA==}
|
||||
dev: true
|
||||
|
@ -3,6 +3,7 @@ import { writable } from "svelte/store";
|
||||
|
||||
export type Project = {
|
||||
id: string;
|
||||
title: string;
|
||||
path: string;
|
||||
};
|
||||
|
||||
|
@ -1,8 +1,60 @@
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import "../app.postcss";
|
||||
|
||||
import { open } from "@tauri-apps/api/dialog";
|
||||
import type { LayoutData } from "./$types";
|
||||
import { nanoid } from "nanoid";
|
||||
import { path } from "@tauri-apps/api";
|
||||
|
||||
export let data: LayoutData;
|
||||
const projects = data.projects;
|
||||
|
||||
const onSelectProjectClick = async () => {
|
||||
const selectedPath = await open({
|
||||
directory: true,
|
||||
recursive: true,
|
||||
});
|
||||
if (selectedPath === null) return;
|
||||
if (Array.isArray(selectedPath) && selectedPath.length !== 1) return;
|
||||
const projectPath = Array.isArray(selectedPath)
|
||||
? selectedPath[0]
|
||||
: selectedPath;
|
||||
|
||||
const projectExists = $projects.some((p) => p.path === projectPath);
|
||||
if (projectExists) return;
|
||||
|
||||
const title = await path.basename(projectPath);
|
||||
$projects = [
|
||||
...$projects,
|
||||
{
|
||||
id: nanoid(),
|
||||
title,
|
||||
path: projectPath,
|
||||
},
|
||||
];
|
||||
};
|
||||
</script>
|
||||
|
||||
<header data-tauri-drag-region class="h-8" />
|
||||
<main>
|
||||
<header data-tauri-drag-region class="ml-16 h-8">
|
||||
<nav class="flex flex-row m-2">
|
||||
<ul class="flex-1 flex flex-row gap-2 overflow-x-scroll">
|
||||
{#each $projects as project}
|
||||
<li class="border rounded-md bg-blue-100 p-2">
|
||||
<a href="/projects/{project.id}/">{project.title}</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
<button
|
||||
class="rounded-lg bg-green-100 p-1 m-1"
|
||||
on:click={onSelectProjectClick}
|
||||
type="button"
|
||||
>
|
||||
new project
|
||||
</button>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main class="p-2">
|
||||
<slot />
|
||||
</main>
|
||||
|
@ -1,108 +1 @@
|
||||
<script lang="ts">
|
||||
import { open } from "@tauri-apps/api/dialog";
|
||||
import { derived, writable } from "svelte/store";
|
||||
import { EventType, watch, type Event } from "$lib/watch";
|
||||
import { TextDocument } from "$lib/crdt";
|
||||
import { NoSuchFileOrDirectoryError, readFile } from "$lib/tauri";
|
||||
import { Timeline } from "$lib/components";
|
||||
import { git } from "$lib";
|
||||
|
||||
const selectedPath = writable<string | string[] | null>(null);
|
||||
|
||||
const onSelectProjectClick = () =>
|
||||
open({
|
||||
directory: true,
|
||||
recursive: true,
|
||||
}).then(selectedPath.set);
|
||||
|
||||
const docs = writable<Record<string, TextDocument>>({});
|
||||
|
||||
const deleteDocs = (...filepaths: string[]) => {
|
||||
$docs = Object.fromEntries(
|
||||
Object.entries($docs).filter(
|
||||
([filepath, _]) => !filepaths.includes(filepath)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const upsertDoc = (filepath: string) =>
|
||||
readFile(filepath)
|
||||
.then((content) => {
|
||||
if (filepath in $docs) {
|
||||
$docs[filepath].update(content);
|
||||
$docs = $docs;
|
||||
} else {
|
||||
$docs[filepath] = TextDocument.new(content);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err instanceof NoSuchFileOrDirectoryError) {
|
||||
deleteDocs(filepath);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
|
||||
const onEvent = (event: Event) => {
|
||||
const isFileCreate =
|
||||
EventType.isCreate(event.type) && event.type.create.kind === "file";
|
||||
const isFileUpdate =
|
||||
EventType.isModify(event.type) && event.type.modify.kind === "data";
|
||||
const isFileRemove = EventType.isRemove(event.type);
|
||||
|
||||
if (isFileCreate) {
|
||||
event.paths.forEach(upsertDoc);
|
||||
} else if (isFileUpdate) {
|
||||
event.paths.forEach(upsertDoc);
|
||||
} else if (isFileRemove) {
|
||||
deleteDocs(...event.paths);
|
||||
}
|
||||
};
|
||||
|
||||
selectedPath.subscribe(async (path) => {
|
||||
if (path === null) return;
|
||||
return await watch(path, onEvent);
|
||||
});
|
||||
|
||||
const timestamps = derived(docs, (docs) =>
|
||||
Object.values(docs).flatMap((doc) =>
|
||||
doc.getHistory().map((h) => h.time)
|
||||
)
|
||||
);
|
||||
|
||||
const min = derived(timestamps, (timestamps) => Math.min(...timestamps));
|
||||
const max = derived(timestamps, (timestamps) => Math.max(...timestamps));
|
||||
|
||||
const showTimeline = derived(
|
||||
[min, max],
|
||||
([min, max]) => isFinite(min) && isFinite(max)
|
||||
);
|
||||
|
||||
git.exec("version").then(console.log);
|
||||
|
||||
let value: number | undefined;
|
||||
</script>
|
||||
|
||||
<form class="flex flex-col">
|
||||
<input class="flex-1" type="text" value={$selectedPath} disabled />
|
||||
<button class="shadow-md" on:click={onSelectProjectClick} type="button">
|
||||
select project
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<ul class="flex flex-col gap-2">
|
||||
{#if $showTimeline}
|
||||
<Timeline min={$min} max={$max} bind:value />
|
||||
{/if}
|
||||
|
||||
{#each Object.entries($docs) as [filepath, doc]}
|
||||
<li>
|
||||
<details open>
|
||||
<summary>{filepath}</summary>
|
||||
<code>
|
||||
{value ? doc.at(value).toString() : doc.toString()}
|
||||
</code>
|
||||
</details>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
overview page
|
||||
|
92
src/routes/projects/[id]/+page.svelte
Normal file
92
src/routes/projects/[id]/+page.svelte
Normal file
@ -0,0 +1,92 @@
|
||||
<script lang="ts">
|
||||
import { derived, writable } from "svelte/store";
|
||||
import { EventType, watch, type Event } from "$lib/watch";
|
||||
import { TextDocument } from "$lib/crdt";
|
||||
import { NoSuchFileOrDirectoryError, readFile } from "$lib/tauri";
|
||||
import type { PageData } from "./$types";
|
||||
import { Timeline } from "$lib/components";
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
const docs = writable<Record<string, TextDocument>>({});
|
||||
|
||||
const deleteDocs = (...filepaths: string[]) => {
|
||||
$docs = Object.fromEntries(
|
||||
Object.entries($docs).filter(
|
||||
([filepath, _]) => !filepaths.includes(filepath)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const upsertDoc = (filepath: string) =>
|
||||
readFile(filepath)
|
||||
.then((content) => {
|
||||
if (filepath in $docs) {
|
||||
$docs[filepath].update(content);
|
||||
$docs = $docs;
|
||||
} else {
|
||||
$docs[filepath] = TextDocument.new(content);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err instanceof NoSuchFileOrDirectoryError) {
|
||||
deleteDocs(filepath);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
|
||||
const onEvent = (event: Event) => {
|
||||
const isFileCreate =
|
||||
EventType.isCreate(event.type) && event.type.create.kind === "file";
|
||||
const isFileUpdate =
|
||||
EventType.isModify(event.type) && event.type.modify.kind === "data";
|
||||
const isFileRemove = EventType.isRemove(event.type);
|
||||
|
||||
if (isFileCreate) {
|
||||
event.paths.forEach(upsertDoc);
|
||||
} else if (isFileUpdate) {
|
||||
event.paths.forEach(upsertDoc);
|
||||
} else if (isFileRemove) {
|
||||
deleteDocs(...event.paths);
|
||||
}
|
||||
};
|
||||
|
||||
$: data.project?.subscribe(async (project) => {
|
||||
if (project === undefined) return;
|
||||
return await watch(project.path, onEvent);
|
||||
});
|
||||
|
||||
const timestamps = derived(docs, (docs) =>
|
||||
Object.values(docs).flatMap((doc) =>
|
||||
doc.getHistory().map((h) => h.time)
|
||||
)
|
||||
);
|
||||
|
||||
const min = derived(timestamps, (timestamps) => Math.min(...timestamps));
|
||||
const max = derived(timestamps, (timestamps) => Math.max(...timestamps));
|
||||
|
||||
const showTimeline = derived(
|
||||
[min, max],
|
||||
([min, max]) => isFinite(min) && isFinite(max)
|
||||
);
|
||||
|
||||
let value: number | undefined;
|
||||
</script>
|
||||
|
||||
<ul class="flex flex-col gap-2">
|
||||
{#if $showTimeline}
|
||||
<Timeline min={$min} max={$max} bind:value />
|
||||
{/if}
|
||||
|
||||
{#each Object.entries($docs) as [filepath, doc]}
|
||||
<li>
|
||||
<details open>
|
||||
<summary>{filepath}</summary>
|
||||
<code>
|
||||
{value ? doc.at(value).toString() : doc.toString()}
|
||||
</code>
|
||||
</details>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
11
src/routes/projects/[id]/+page.ts
Normal file
11
src/routes/projects/[id]/+page.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { derived } from "svelte/store";
|
||||
import type { PageLoad } from "./$types";
|
||||
|
||||
export const load: PageLoad = async ({ parent, params }) => {
|
||||
const { projects } = await parent();
|
||||
return {
|
||||
project: derived(projects, (projects) =>
|
||||
projects.find((project) => project.id === params.id)
|
||||
),
|
||||
};
|
||||
};
|
Loading…
Reference in New Issue
Block a user