project page

This commit is contained in:
Nikita Galaiko 2023-02-03 09:12:57 +01:00
parent bf422c027a
commit 76d3b31054
No known key found for this signature in database
GPG Key ID: EBAB54E845BA519D
7 changed files with 169 additions and 111 deletions

View File

@ -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"
},

View File

@ -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

View File

@ -3,6 +3,7 @@ import { writable } from "svelte/store";
export type Project = {
id: string;
title: string;
path: string;
};

View File

@ -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>

View File

@ -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

View 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>

View 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)
),
};
};