simple timeline

This commit is contained in:
Nikita Galaiko 2023-02-01 15:10:22 +01:00
parent a66d88d80b
commit 33904f6533
No known key found for this signature in database
GPG Key ID: EBAB54E845BA519D
9 changed files with 283 additions and 62 deletions

View File

@ -15,6 +15,7 @@
"@tauri-apps/api": "^1.2.0",
"diff": "^5.1.0",
"idb-keyval": "^6.2.0",
"mm-jsr": "^3.0.2",
"tauri-plugin-fs-watch-api": "github:tauri-apps/tauri-plugin-fs-watch",
"yjs": "^13.5.45"
},

View File

@ -9,6 +9,7 @@ specifiers:
autoprefixer: ^10.4.7
diff: ^5.1.0
idb-keyval: ^6.2.0
mm-jsr: ^3.0.2
postcss: ^8.4.14
postcss-load-config: ^4.0.1
svelte: ^3.54.0
@ -25,7 +26,8 @@ dependencies:
'@tauri-apps/api': 1.2.0
diff: 5.1.0
idb-keyval: 6.2.0
tauri-plugin-fs-watch-api: github.com/tauri-apps/tauri-plugin-fs-watch/6ba8af302bf10e0312584c2d43f29b02cd063e1d
mm-jsr: 3.0.2
tauri-plugin-fs-watch-api: github.com/tauri-apps/tauri-plugin-fs-watch/7d63ad9dfd72ae6c2bb05c148f344adb0521ec3a
yjs: 13.5.45
devDependencies:
@ -922,6 +924,10 @@ packages:
minimist: 1.2.7
dev: true
/mm-jsr/3.0.2:
resolution: {integrity: sha512-ATbSVKgOU9i54eBLPV+QETFKhGODnCDKsi18TLsET7BCJnX00LjcbOZYvw6ODplJpRY7JCrA861mYfViCDnh3w==}
dev: false
/mri/1.2.0:
resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==}
engines: {node: '>=4'}
@ -1524,8 +1530,8 @@ packages:
lib0: 0.2.60
dev: false
github.com/tauri-apps/tauri-plugin-fs-watch/6ba8af302bf10e0312584c2d43f29b02cd063e1d:
resolution: {tarball: https://codeload.github.com/tauri-apps/tauri-plugin-fs-watch/tar.gz/6ba8af302bf10e0312584c2d43f29b02cd063e1d}
github.com/tauri-apps/tauri-plugin-fs-watch/7d63ad9dfd72ae6c2bb05c148f344adb0521ec3a:
resolution: {tarball: https://codeload.github.com/tauri-apps/tauri-plugin-fs-watch/tar.gz/7d63ad9dfd72ae6c2bb05c148f344adb0521ec3a}
name: tauri-plugin-fs-watch-api
version: 0.0.0
dependencies:

1
src-tauri/file.txt Normal file
View File

@ -0,0 +1 @@
hello world

View File

@ -1,25 +0,0 @@
<script lang="ts">
import { open, type OpenDialogOptions } from "@tauri-apps/api/dialog";
import { createEventDispatcher } from "svelte";
const dispatch = createEventDispatcher<{ select: { path: string } }>();
export let filters: OpenDialogOptions["filters"] = [];
const onButtonClick = () =>
open({
filters,
}).then((selected) => {
if (!Array.isArray(selected) && selected !== null) {
dispatch("select", { path: selected });
}
});
</script>
<button
type="button"
class="shadow-md py-1 px-2 rounded-md transition hover:scale-105"
on:click={onButtonClick}
>
<slot>select</slot>
</button>

View File

@ -0,0 +1,27 @@
<script lang="ts">
import type { TextDocument } from "$lib/crdt";
import { Timeline } from "$lib/components";
export let doc: TextDocument;
$: min = Math.min(...doc.getHistory().map((entry) => entry.time));
$: max = Math.max(...doc.getHistory().map((entry) => entry.time));
$: initValue = doc.getHistory().at(-1)?.time;
let value: number | undefined;
$: display = value ? doc.at(value).toString() : doc.toString();
$: console.log({ value, min, max, display });
</script>
<figure class="flex flex-col gap-2">
<figcaption class="m-auto">
{#if isFinite(min) && isFinite(max) && min !== max && initValue}
<Timeline bind:min bind:max bind:value {initValue} />
{/if}
</figcaption>
<code>
{display}
</code>
</figure>

View File

@ -0,0 +1,183 @@
<script lang="ts">
import {
JSR,
ModuleSlider,
ModuleBar,
ModuleLabel,
ModuleGrid,
} from "mm-jsr";
export let min: number,
max: number,
step: number = 1,
initValue: number,
value: number | undefined,
formatter = (value: number): string => `${value}`;
value = initValue;
const jsr = (
container: HTMLElement,
config: {
min: number;
max: number;
}
) => {
let jsr: JSR | undefined;
const update = (config: { min: number; max: number }) => {
jsr?.destroy();
jsr = new JSR({
modules: [
new ModuleSlider(),
new ModuleBar(),
new ModuleGrid({ formatter }),
new ModuleLabel({ formatter }),
],
config: {
min: config.min,
max: config.max,
step,
initialValues: [initValue],
container: container,
},
});
jsr.onValueChange(({ real }) => {
console.log(real);
value = real;
});
};
update(config);
return {
destroy: jsr?.destroy,
update: (config: { min: number; max: number }) => update(config),
};
};
</script>
<div {...$$restProps}>
<div class="jsr-container" use:jsr={{ min, max }}>
<style>
[class^="jsr"] {
box-sizing: border-box;
}
.jsr {
display: block;
position: relative;
padding-top: 10px;
width: 100%;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
-webkit-touch-callout: none;
-khtml-user-select: none;
font: 14px sans-serif;
}
.jsr.is-disabled {
background: grey;
}
.jsr_rail {
height: 5px;
background: #444;
}
.jsr_slider {
position: absolute;
top: calc(5px / 2 + 10px);
left: 0;
transform: translate(-50%, -50%);
width: 25px;
height: 25px;
cursor: col-resize;
transition: background 0.1s ease-in-out;
outline: 0;
z-index: 3;
}
.jsr_slider:focus {
z-index: 4;
}
.jsr_slider::before {
content: "";
width: 15px;
height: 15px;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: #999;
border-radius: 50%;
}
.jsr_slider:focus::before {
background: #c00;
}
.jsr_label {
position: absolute;
top: calc(10px + 5px + 15px / 1.5);
transform: translateX(-50%);
padding: 0.2em 0.4em;
background: #444;
color: #fff;
font-size: 0.9em;
white-space: nowrap;
border-radius: 0.3em;
z-index: 2;
cursor: col-resize;
opacity: 1;
}
.jsr_label.is-hidden {
opacity: 0;
pointer-events: none;
}
.jsr_bar {
position: absolute;
height: 5px;
background-color: #999;
z-index: 2;
cursor: move;
top: 10px;
}
.jsr_limit {
position: absolute;
z-index: 1;
pointer-events: none;
height: 5px;
top: 10px;
background-color: #727272;
}
.jsr_grid {
margin-top: 5px;
width: 100%;
}
.jsr_lockscreen {
overflow: hidden;
height: 100%;
width: 100%;
}
</style>
</div>
</div>

View File

@ -1 +1,2 @@
export { defautl as DirectorySelector } from "./DirectorySelector.svelte";
export { default as Timeline } from "./Timeline.svelte";
export { default as File } from "./File.svelte";

View File

@ -41,21 +41,54 @@ const getDeltaOperations = (
return deltas;
};
export const text = (content?: string) => {
const doc = new Doc();
const deltas = getDeltaOperations("", content || "");
doc.getText().applyDelta(deltas);
const snapshots = [{ time: new Date().getTime(), deltas }];
return {
update: (content: string) => {
const deltas = getDeltaOperations(doc.getText().toString(), content);
doc.getText().applyDelta(deltas);
snapshots.push({
time: new Date().getTime(),
deltas,
});
},
history: () => snapshots,
toString: () => doc.getText().toString(),
};
};
export class TextDocument {
private doc: Doc = new Doc();
private history: { time: number; deltas: Delta[] }[] = [];
private constructor({
content,
history,
}: {
content?: string;
history: { time: number; deltas: Delta[] }[];
}) {
if (content !== undefined && history.length > 0) {
throw new Error("only one of content and history can be set");
} else if (content !== undefined) {
this.update(content);
} else if (history.length > 0) {
this.doc
.getText()
.applyDelta(
history.sort((a, b) => a.time - b.time).flatMap((h) => h.deltas)
);
this.history = history;
}
}
static new(content?: string) {
return new TextDocument({ content, history: [] });
}
update(content: string) {
const deltas = getDeltaOperations(this.toString(), content);
if (deltas.length == 0) return;
this.doc.getText().applyDelta(deltas);
this.history.push({ time: new Date().getTime(), deltas });
}
getHistory() {
return this.history.slice();
}
toString() {
return this.doc.getText().toString();
}
at(time: number) {
return new TextDocument({
history: this.history.filter((entry) => entry.time <= time),
});
}
}

View File

@ -2,8 +2,9 @@
import { open } from "@tauri-apps/api/dialog";
import { writable } from "svelte/store";
import { EventType, watch, type Event } from "$lib/watch";
import { crdt } from "$lib";
import { TextDocument } from "$lib/crdt";
import { NoSuchFileOrDirectoryError, readFile } from "$lib/tauri";
import { File } from "$lib/components";
const selectedPath = writable<string | string[] | null>(null);
@ -13,7 +14,7 @@
recursive: true,
}).then(selectedPath.set);
const docs = writable<Record<string, ReturnType<typeof crdt.text>>>({});
const docs = writable<Record<string, TextDocument>>({});
const deleteDocs = (...filepaths: string[]) => {
$docs = Object.fromEntries(
@ -28,9 +29,9 @@
.then((content) => {
if (filepath in $docs) {
$docs[filepath].update(content);
$docs[filepath] = $docs[filepath];
$docs = $docs;
} else {
$docs[filepath] = crdt.text(content);
$docs[filepath] = TextDocument.new(content);
}
})
.catch((err) => {
@ -73,19 +74,12 @@
<ul class="flex flex-col gap-2">
{#each Object.entries($docs) as [filepath, doc]}
<li>
<figure>
<figcaption>{filepath}</figcaption>
<details>
<summary>{filepath}</summary>
<ul>
{#each doc.history() as { time, deltas }}
<li>
<time>{time}</time>
<code>
{JSON.stringify(deltas)}
</code>
</li>
{/each}
<File {doc} />
</ul>
</figure>
</details>
</li>
{/each}
</ul>