mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-11-23 20:54:50 +03:00
simple timeline
This commit is contained in:
parent
a66d88d80b
commit
33904f6533
@ -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"
|
||||
},
|
||||
|
@ -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
1
src-tauri/file.txt
Normal file
@ -0,0 +1 @@
|
||||
hello world
|
@ -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>
|
27
src/lib/components/File.svelte
Normal file
27
src/lib/components/File.svelte
Normal 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>
|
183
src/lib/components/Timeline.svelte
Normal file
183
src/lib/components/Timeline.svelte
Normal 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>
|
@ -1 +1,2 @@
|
||||
export { defautl as DirectorySelector } from "./DirectorySelector.svelte";
|
||||
export { default as Timeline } from "./Timeline.svelte";
|
||||
export { default as File } from "./File.svelte";
|
||||
|
@ -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),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user