Add cloud browsing, autosave, and editor improvements
This commit is contained in:
@@ -2,8 +2,9 @@
|
||||
import Icon from "@iconify/svelte";
|
||||
import Modal from "./Modal.svelte";
|
||||
import * as api from "$lib/ts/api";
|
||||
import type { Asset } from "$lib/ts/api";
|
||||
import type { Resource } from "$lib/ts/api";
|
||||
import { pickFiles } from "$lib/ts/import";
|
||||
import { app } from "$lib/ts/state.svelte";
|
||||
|
||||
interface Props {
|
||||
oninsert?: (snippet: string) => void;
|
||||
@@ -13,14 +14,33 @@
|
||||
|
||||
let { oninsert, onchanged, onclose }: Props = $props();
|
||||
|
||||
let assets = $state<Asset[]>([]);
|
||||
let resources = $state<Resource[]>([]);
|
||||
let previews = $state<Record<string, string>>({});
|
||||
let query = $state("");
|
||||
let scope = $state<"all" | "project" | "shared">("all");
|
||||
let busy = $state(false);
|
||||
let error = $state("");
|
||||
let copied = $state<string | null>(null);
|
||||
|
||||
const filtered = $derived(
|
||||
resources.filter((resource) => {
|
||||
if (scope !== "all" && resource.scope !== scope) return false;
|
||||
const needle = query.trim().toLowerCase();
|
||||
if (!needle) return true;
|
||||
return (
|
||||
resource.name.toLowerCase().includes(needle) ||
|
||||
resource.reference.toLowerCase().includes(needle) ||
|
||||
resource.font_families.some((family) =>
|
||||
family.toLowerCase().includes(needle),
|
||||
)
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
async function refresh() {
|
||||
if (!app.target) return;
|
||||
try {
|
||||
assets = await api.listAssets();
|
||||
resources = await api.listResources(app.target.path);
|
||||
} catch (caught) {
|
||||
error = api.errorMessage(caught);
|
||||
}
|
||||
@@ -30,14 +50,42 @@
|
||||
refresh();
|
||||
});
|
||||
|
||||
async function importFiles() {
|
||||
$effect(() => {
|
||||
const images = filtered.filter((resource) => resource.kind === "image");
|
||||
let cancelled = false;
|
||||
|
||||
(async () => {
|
||||
for (const resource of images) {
|
||||
if (cancelled) return;
|
||||
if (previews[resource.path]) continue;
|
||||
try {
|
||||
const result = await api.thumbnail(resource.path);
|
||||
if (!cancelled && result.kind === "image") {
|
||||
previews[resource.path] = result.data;
|
||||
}
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
});
|
||||
|
||||
async function importInto(destination: "project" | "shared") {
|
||||
const sources = await pickFiles("assets");
|
||||
if (sources.length === 0) return;
|
||||
|
||||
busy = true;
|
||||
error = "";
|
||||
try {
|
||||
await api.importAssets(sources);
|
||||
if (destination === "shared") {
|
||||
await api.importAssets(sources);
|
||||
} else if (app.target) {
|
||||
await api.importIntoTarget(app.target.path, sources);
|
||||
}
|
||||
await refresh();
|
||||
onchanged?.();
|
||||
} catch (caught) {
|
||||
@@ -47,9 +95,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function remove(asset: Asset) {
|
||||
async function remove(resource: Resource) {
|
||||
try {
|
||||
await api.deleteAsset(asset.name);
|
||||
if (resource.scope === "shared") {
|
||||
await api.deleteAsset(resource.name);
|
||||
} else {
|
||||
await api.deleteEntry(resource.path);
|
||||
}
|
||||
delete previews[resource.path];
|
||||
await refresh();
|
||||
onchanged?.();
|
||||
} catch (caught) {
|
||||
@@ -57,17 +110,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
function insert(asset: Asset) {
|
||||
if (asset.kind === "image") {
|
||||
oninsert?.(`#image("${asset.name}")`);
|
||||
} else if (asset.font_families.length > 0) {
|
||||
oninsert?.(`#set text(font: "${asset.font_families[0]}")`);
|
||||
function insert(resource: Resource) {
|
||||
if (resource.kind === "image") {
|
||||
oninsert?.(`#image("${resource.reference}")`);
|
||||
} else if (resource.font_families.length > 0) {
|
||||
oninsert?.(`#set text(font: "${resource.font_families[0]}")`);
|
||||
} else {
|
||||
oninsert?.(`"${resource.reference}"`);
|
||||
}
|
||||
}
|
||||
|
||||
async function copyFamily(family: string) {
|
||||
await navigator.clipboard.writeText(family);
|
||||
copied = family;
|
||||
async function copyReference(value: string) {
|
||||
await navigator.clipboard.writeText(value);
|
||||
copied = value;
|
||||
setTimeout(() => (copied = null), 1200);
|
||||
}
|
||||
|
||||
@@ -77,80 +132,121 @@
|
||||
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
||||
}
|
||||
|
||||
const iconFor: Record<Asset["kind"], string> = {
|
||||
const iconFor: Record<string, string> = {
|
||||
image: "ph:image",
|
||||
font: "ph:text-aa",
|
||||
file: "ph:file",
|
||||
};
|
||||
</script>
|
||||
|
||||
<Modal title="Images and fonts" icon="ph:images" width="max-w-2xl" {onclose}>
|
||||
<div class="flex flex-col gap-4">
|
||||
<p class="text-xs text-[var(--color-ink-muted)]">
|
||||
Files imported here are available to every project. Reference an image by
|
||||
its file name, and a font by its family name.
|
||||
</p>
|
||||
<Modal title="Assets" icon="ph:images" width="max-w-3xl" {onclose}>
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<div
|
||||
class="flex flex-1 items-center gap-2 rounded-md border border-[var(--color-line)] bg-[var(--color-surface)] px-3 py-2 focus-within:border-[var(--color-accent)]"
|
||||
>
|
||||
<Icon icon="ph:magnifying-glass" class="text-[var(--color-ink-muted)]" />
|
||||
<input
|
||||
class="min-w-0 flex-1 bg-transparent text-sm focus:outline-none"
|
||||
placeholder="Search images, fonts, and files"
|
||||
bind:value={query}
|
||||
/>
|
||||
{#if query}
|
||||
<button
|
||||
class="text-[var(--color-ink-muted)] hover:text-[var(--color-ink)]"
|
||||
onclick={() => (query = "")}
|
||||
aria-label="Clear search"
|
||||
>
|
||||
<Icon icon="ph:x" />
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex rounded-md bg-[var(--color-surface-sunken)] p-0.5 text-xs">
|
||||
{#each [["all", "All"], ["project", "This project"], ["shared", "Shared"]] as [value, label]}
|
||||
<button
|
||||
class="rounded px-2.5 py-1.5 transition
|
||||
{scope === value
|
||||
? 'bg-[var(--color-surface)] font-medium shadow-sm'
|
||||
: 'text-[var(--color-ink-muted)] hover:text-[var(--color-ink)]'}"
|
||||
onclick={() => (scope = value as "all" | "project" | "shared")}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<p class="text-xs text-[var(--color-danger)]">{error}</p>
|
||||
{/if}
|
||||
|
||||
{#if assets.length === 0}
|
||||
{#if filtered.length === 0}
|
||||
<div
|
||||
class="flex flex-col items-center gap-3 rounded-lg border border-dashed border-[var(--color-line)] px-4 py-10 text-center"
|
||||
>
|
||||
<Icon icon="ph:image-square" class="text-4xl text-[var(--color-ink-muted)]" />
|
||||
<p class="text-xs text-[var(--color-ink-muted)]">
|
||||
No images or fonts imported yet.
|
||||
{query ? "Nothing matches that search." : "No images or fonts yet."}
|
||||
</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-col gap-1">
|
||||
{#each assets as asset (asset.name)}
|
||||
<div
|
||||
class="scroll-thin grid max-h-96 grid-cols-[repeat(auto-fill,minmax(150px,1fr))] gap-2 overflow-y-auto"
|
||||
>
|
||||
{#each filtered as resource (resource.path)}
|
||||
<div
|
||||
class="group flex items-center gap-3 rounded-md border border-[var(--color-line)] px-3 py-2"
|
||||
class="group relative flex flex-col overflow-hidden rounded-lg border border-[var(--color-line)] transition hover:border-[var(--color-accent)]"
|
||||
>
|
||||
<Icon
|
||||
icon={iconFor[asset.kind]}
|
||||
class="text-lg text-[var(--color-accent)]"
|
||||
/>
|
||||
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="truncate text-xs font-medium">{asset.name}</p>
|
||||
{#if asset.font_families.length > 0}
|
||||
<div class="mt-0.5 flex flex-wrap gap-1">
|
||||
{#each asset.font_families as family}
|
||||
<button
|
||||
class="rounded bg-[var(--color-surface-sunken)] px-1.5 py-px text-[10px] text-[var(--color-ink-muted)] hover:text-[var(--color-ink)]"
|
||||
onclick={() => copyFamily(family)}
|
||||
title="Copy family name"
|
||||
>
|
||||
{copied === family ? "Copied" : family}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
<button
|
||||
class="flex h-20 items-center justify-center overflow-hidden bg-[var(--color-surface-muted)]"
|
||||
onclick={() => insert(resource)}
|
||||
title="Insert into document"
|
||||
>
|
||||
{#if previews[resource.path]}
|
||||
<img
|
||||
src={previews[resource.path]}
|
||||
alt={resource.name}
|
||||
class="h-full w-full object-contain"
|
||||
/>
|
||||
{:else}
|
||||
<p class="text-[10px] text-[var(--color-ink-muted)]">
|
||||
{formatSize(asset.size)}
|
||||
</p>
|
||||
<Icon
|
||||
icon={iconFor[resource.kind] ?? "ph:file"}
|
||||
class="text-2xl text-[var(--color-accent)]"
|
||||
/>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
<div class="flex flex-col gap-0.5 px-2 py-1.5">
|
||||
<span class="truncate text-[11px] font-medium" title={resource.reference}>
|
||||
{resource.name}
|
||||
</span>
|
||||
|
||||
{#if resource.font_families.length > 0}
|
||||
<button
|
||||
class="truncate text-left text-[10px] text-[var(--color-ink-muted)] hover:text-[var(--color-ink)]"
|
||||
onclick={() => copyReference(resource.font_families[0])}
|
||||
title="Copy family name"
|
||||
>
|
||||
{copied === resource.font_families[0]
|
||||
? "Copied"
|
||||
: resource.font_families[0]}
|
||||
</button>
|
||||
{:else}
|
||||
<span class="text-[10px] text-[var(--color-ink-muted)]">
|
||||
{resource.scope === "shared" ? "Shared" : "Project"} · {formatSize(
|
||||
resource.size,
|
||||
)}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if oninsert && (asset.kind === "image" || asset.font_families.length > 0)}
|
||||
<button
|
||||
class="rounded border border-[var(--color-line)] px-2 py-1 text-[10px] opacity-0 transition group-hover:opacity-100 hover:bg-[var(--color-surface-muted)]"
|
||||
onclick={() => insert(asset)}
|
||||
>
|
||||
Insert
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
class="rounded p-1 text-[var(--color-ink-muted)] opacity-0 transition group-hover:opacity-100 hover:bg-[var(--color-surface-sunken)] hover:text-[var(--color-danger)]"
|
||||
onclick={() => remove(asset)}
|
||||
class="absolute right-1 top-1 rounded bg-[var(--color-surface)]/90 p-1 text-[var(--color-ink-muted)] opacity-0 transition group-hover:opacity-100 hover:text-[var(--color-danger)]"
|
||||
onclick={() => remove(resource)}
|
||||
aria-label="Delete"
|
||||
>
|
||||
<Icon icon="ph:trash" />
|
||||
<Icon icon="ph:trash" class="text-xs" />
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
@@ -166,12 +262,20 @@
|
||||
Close
|
||||
</button>
|
||||
<button
|
||||
class="flex items-center gap-1.5 rounded-md bg-[var(--color-accent)] px-3 py-1.5 text-xs font-medium text-white transition hover:opacity-90 disabled:opacity-50"
|
||||
class="flex items-center gap-1.5 rounded-md border border-[var(--color-line)] px-3 py-1.5 text-xs transition hover:bg-[var(--color-surface-muted)] disabled:opacity-50"
|
||||
disabled={busy}
|
||||
onclick={importFiles}
|
||||
onclick={() => importInto("shared")}
|
||||
>
|
||||
<Icon icon="ph:upload-simple" />
|
||||
{busy ? "Importing..." : "Import files"}
|
||||
Add to shared
|
||||
</button>
|
||||
<button
|
||||
class="flex items-center gap-1.5 rounded-md bg-[var(--color-accent)] px-3 py-1.5 text-xs font-medium text-white transition hover:opacity-90 disabled:opacity-50"
|
||||
disabled={busy}
|
||||
onclick={() => importInto("project")}
|
||||
>
|
||||
<Icon icon="ph:upload-simple" />
|
||||
{busy ? "Importing..." : "Add to project"}
|
||||
</button>
|
||||
{/snippet}
|
||||
</Modal>
|
||||
|
||||
@@ -6,7 +6,9 @@
|
||||
app,
|
||||
breadcrumbs,
|
||||
browseTo,
|
||||
openCloudFolder,
|
||||
openTarget,
|
||||
refreshCloud,
|
||||
} from "$lib/ts/state.svelte";
|
||||
|
||||
interface Props {
|
||||
@@ -14,11 +16,11 @@
|
||||
onnewproject: () => void;
|
||||
onnewdocument: () => void;
|
||||
onupload: () => void;
|
||||
onassets: () => void;
|
||||
onrename: (entry: BrowseEntry) => void;
|
||||
ondelete: (entry: BrowseEntry) => void;
|
||||
onlink: (entry: BrowseEntry) => void;
|
||||
onviewimage: (paths: string[], index: number) => void;
|
||||
ondownloaddocument: (documentId: string, title: string) => void;
|
||||
onclonespace: (spaceId: string, name: string) => void;
|
||||
ondeletespace: (spaceId: string) => void;
|
||||
onnewspace: () => void;
|
||||
@@ -30,11 +32,11 @@
|
||||
onnewproject,
|
||||
onnewdocument,
|
||||
onupload,
|
||||
onassets,
|
||||
onrename,
|
||||
ondelete,
|
||||
onlink,
|
||||
onviewimage,
|
||||
ondownloaddocument,
|
||||
onclonespace,
|
||||
ondeletespace,
|
||||
onnewspace,
|
||||
@@ -45,6 +47,12 @@
|
||||
|
||||
const trail = $derived(breadcrumbs());
|
||||
|
||||
$effect(() => {
|
||||
if (app.scope === "cloud" && app.account) {
|
||||
refreshCloud();
|
||||
}
|
||||
});
|
||||
|
||||
const containers = $derived(
|
||||
app.entries.filter(
|
||||
(entry) => entry.kind === "folder" || entry.kind === "project",
|
||||
@@ -221,13 +229,6 @@
|
||||
<div class="flex-1"></div>
|
||||
|
||||
{#if app.scope === "local"}
|
||||
<button
|
||||
class="flex items-center gap-1.5 rounded-md px-2.5 py-1.5 text-xs text-[var(--color-ink-muted)] transition hover:bg-[var(--color-surface-muted)] hover:text-[var(--color-ink)]"
|
||||
onclick={onassets}
|
||||
>
|
||||
<Icon icon="ph:images" />
|
||||
Assets
|
||||
</button>
|
||||
<button
|
||||
class="flex items-center gap-1.5 rounded-md px-2.5 py-1.5 text-xs text-[var(--color-ink-muted)] transition hover:bg-[var(--color-surface-muted)] hover:text-[var(--color-ink)]"
|
||||
onclick={onupload}
|
||||
@@ -454,14 +455,105 @@
|
||||
Sign in
|
||||
</button>
|
||||
</div>
|
||||
{:else if app.spaces.length === 0}
|
||||
<div
|
||||
class="flex h-full flex-col items-center justify-center gap-3 text-[var(--color-ink-muted)]"
|
||||
>
|
||||
<Icon icon="ph:cloud" class="text-5xl" />
|
||||
<p class="text-sm">No cloud spaces yet.</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="mb-3 flex items-center gap-1 text-xs">
|
||||
<button
|
||||
class="flex items-center gap-1.5 rounded px-2 py-1 transition hover:bg-[var(--color-surface)]
|
||||
{app.cloudFolder === null ? 'font-medium' : 'text-[var(--color-ink-muted)]'}"
|
||||
onclick={() => openCloudFolder(null)}
|
||||
>
|
||||
<Icon icon="ph:cloud" />
|
||||
My Drive
|
||||
</button>
|
||||
<button
|
||||
class="flex items-center gap-1.5 rounded px-2 py-1 transition hover:bg-[var(--color-surface)]
|
||||
{app.cloudFolder === 'shared'
|
||||
? 'font-medium'
|
||||
: 'text-[var(--color-ink-muted)]'}"
|
||||
onclick={() => openCloudFolder("shared")}
|
||||
>
|
||||
<Icon icon="ph:users-three" />
|
||||
Shared with me
|
||||
</button>
|
||||
|
||||
{#if app.cloudLoading}
|
||||
<Icon
|
||||
icon="ph:circle-notch"
|
||||
class="animate-spin text-[var(--color-accent)]"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if app.cloudFolders.length > 0}
|
||||
<div
|
||||
class="mb-4 grid grid-cols-[repeat(auto-fill,minmax(210px,1fr))] gap-2"
|
||||
>
|
||||
{#each app.cloudFolders as folder (folder.id)}
|
||||
<button
|
||||
class="flex items-center gap-2.5 rounded-lg border border-[var(--color-line)] bg-[var(--color-surface-sunken)] px-3 py-2.5 text-left transition hover:border-[var(--color-accent)] hover:bg-[var(--color-surface)]"
|
||||
onclick={() => openCloudFolder(folder.id)}
|
||||
>
|
||||
<Icon
|
||||
icon="ph:folder-fill"
|
||||
class="shrink-0 text-2xl text-[var(--color-ink-muted)]"
|
||||
/>
|
||||
<span class="truncate text-xs font-medium">{folder.name}</span>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if app.cloudDocuments.length > 0}
|
||||
<h2
|
||||
class="mb-2 text-[10px] font-semibold uppercase tracking-wider text-[var(--color-ink-muted)]"
|
||||
>
|
||||
Documents
|
||||
</h2>
|
||||
<div
|
||||
class="mb-4 grid grid-cols-[repeat(auto-fill,minmax(200px,1fr))] gap-3"
|
||||
>
|
||||
{#each app.cloudDocuments as document (document.id)}
|
||||
<div
|
||||
class="flex flex-col gap-2 rounded-lg border border-[var(--color-line)] bg-[var(--color-surface)] p-3 transition hover:border-[var(--color-accent)]"
|
||||
>
|
||||
<Icon
|
||||
icon="ph:file-text"
|
||||
class="text-xl text-[var(--color-accent)]"
|
||||
/>
|
||||
<span class="truncate text-xs font-medium" title={document.title}>
|
||||
{document.title}
|
||||
</span>
|
||||
<span class="text-[10px] text-[var(--color-ink-muted)]">
|
||||
{document.role} · {formatDate(document.updated_at)}
|
||||
</span>
|
||||
<button
|
||||
class="mt-1 flex items-center justify-center gap-1 rounded border border-[var(--color-line)] px-2 py-1 text-[10px] hover:bg-[var(--color-surface-muted)]"
|
||||
onclick={() => ondownloaddocument(document.id, document.title)}
|
||||
>
|
||||
<Icon icon="ph:download-simple" />
|
||||
Download
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if app.spaces.length === 0 && app.cloudDocuments.length === 0 && app.cloudFolders.length === 0}
|
||||
<div
|
||||
class="flex flex-col items-center justify-center gap-3 py-16 text-[var(--color-ink-muted)]"
|
||||
>
|
||||
<Icon icon="ph:cloud" class="text-5xl" />
|
||||
<p class="text-sm">Nothing here yet.</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if app.spaces.length > 0}
|
||||
<h2
|
||||
class="mb-2 text-[10px] font-semibold uppercase tracking-wider text-[var(--color-ink-muted)]"
|
||||
>
|
||||
Spaces
|
||||
</h2>
|
||||
{/if}
|
||||
<div class="grid grid-cols-[repeat(auto-fill,minmax(220px,1fr))] gap-3">
|
||||
{#each app.spaces as space (space.id)}
|
||||
<div
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
<script lang="ts">
|
||||
import Icon from "@iconify/svelte";
|
||||
import { openUrl } from "@tauri-apps/plugin-opener";
|
||||
import Modal from "./Modal.svelte";
|
||||
import * as api from "$lib/ts/api";
|
||||
import type { AppInfo } from "$lib/ts/api";
|
||||
|
||||
interface Props {
|
||||
onclose: () => void;
|
||||
}
|
||||
|
||||
let { onclose }: Props = $props();
|
||||
|
||||
let info = $state<AppInfo | null>(null);
|
||||
|
||||
$effect(() => {
|
||||
api
|
||||
.appInfo()
|
||||
.then((result) => (info = result))
|
||||
.catch(() => (info = null));
|
||||
});
|
||||
|
||||
const links = [
|
||||
{
|
||||
label: "Typst Documentation",
|
||||
url: "https://typst.app/docs/",
|
||||
icon: "ph:book-open",
|
||||
},
|
||||
{
|
||||
label: "Typst Universe",
|
||||
url: "https://typst.app/universe/",
|
||||
icon: "ph:planet",
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<Modal title="About Typst Desktop" icon="ph:info" {onclose}>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<Icon icon="ph:file-code" class="text-3xl text-[var(--color-accent)]" />
|
||||
<div>
|
||||
<p class="text-sm font-semibold">Typst Desktop</p>
|
||||
<p class="text-xs text-[var(--color-ink-muted)]">
|
||||
A local editor for Typst documents
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if info}
|
||||
<dl class="flex flex-col gap-1 text-xs">
|
||||
<div class="flex justify-between border-b border-[var(--color-line)] py-1.5">
|
||||
<dt class="text-[var(--color-ink-muted)]">Version</dt>
|
||||
<dd class="font-medium">{info.version}</dd>
|
||||
</div>
|
||||
<div class="flex justify-between border-b border-[var(--color-line)] py-1.5">
|
||||
<dt class="text-[var(--color-ink-muted)]">Typst</dt>
|
||||
<dd class="font-medium">{info.typst_version}</dd>
|
||||
</div>
|
||||
<div class="flex justify-between border-b border-[var(--color-line)] py-1.5">
|
||||
<dt class="text-[var(--color-ink-muted)]">Tauri</dt>
|
||||
<dd class="font-medium">{info.tauri_version}</dd>
|
||||
</div>
|
||||
<div class="flex justify-between border-b border-[var(--color-line)] py-1.5">
|
||||
<dt class="text-[var(--color-ink-muted)]">Author</dt>
|
||||
<dd class="font-medium">{info.authors}</dd>
|
||||
</div>
|
||||
<div class="flex justify-between py-1.5">
|
||||
<dt class="text-[var(--color-ink-muted)]">License</dt>
|
||||
<dd class="font-medium">{info.license}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
{/if}
|
||||
|
||||
<div class="flex flex-col gap-1.5">
|
||||
{#each links as link}
|
||||
<button
|
||||
class="flex items-center gap-2 rounded-md border border-[var(--color-line)] px-3 py-2 text-xs transition hover:border-[var(--color-accent)] hover:bg-[var(--color-surface-muted)]"
|
||||
onclick={() => openUrl(link.url)}
|
||||
>
|
||||
<Icon icon={link.icon} class="text-base text-[var(--color-accent)]" />
|
||||
<span class="flex-1 text-left">{link.label}</span>
|
||||
<Icon
|
||||
icon="ph:arrow-square-out"
|
||||
class="text-[var(--color-ink-muted)]"
|
||||
/>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<p
|
||||
class="rounded-md border border-[var(--color-line)] bg-[var(--color-surface-muted)] p-3 text-[11px] leading-relaxed text-[var(--color-ink-muted)]"
|
||||
>
|
||||
Typst Desktop is a community application and is not affiliated with,
|
||||
endorsed by, or supported by the official Typst project. The links above
|
||||
open the official Typst website in your browser.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{#snippet footer()}
|
||||
<button
|
||||
class="rounded-md bg-[var(--color-accent)] px-3 py-1.5 text-xs font-medium text-white transition hover:opacity-90"
|
||||
onclick={onclose}
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
{/snippet}
|
||||
</Modal>
|
||||
@@ -3,7 +3,14 @@
|
||||
import { open } from "@tauri-apps/plugin-dialog";
|
||||
import Modal from "./Modal.svelte";
|
||||
import * as api from "$lib/ts/api";
|
||||
import { app, applyTheme, refreshEntries, setError } from "$lib/ts/state.svelte";
|
||||
import { untrack } from "svelte";
|
||||
import {
|
||||
app,
|
||||
applyTheme,
|
||||
refreshEntries,
|
||||
restartAutoSync,
|
||||
setError,
|
||||
} from "$lib/ts/state.svelte";
|
||||
|
||||
interface Props {
|
||||
onclose: () => void;
|
||||
@@ -12,10 +19,26 @@
|
||||
|
||||
let { onclose, onsignin }: Props = $props();
|
||||
|
||||
let workspaceRoot = $state(app.settings?.workspace_root ?? "");
|
||||
let serverUrl = $state(app.settings?.server_url ?? "");
|
||||
let workspaceRoot = $state(untrack(() => app.settings?.workspace_root ?? ""));
|
||||
let serverUrl = $state(untrack(() => app.settings?.server_url ?? ""));
|
||||
let autosaveSeconds = $state(untrack(() => app.settings?.autosave_seconds ?? 0));
|
||||
let syncMinutes = $state(untrack(() => app.settings?.sync_minutes ?? 0));
|
||||
let saving = $state(false);
|
||||
|
||||
const autosaveOptions = [
|
||||
{ value: 0, label: "Off" },
|
||||
{ value: 5, label: "5 seconds" },
|
||||
{ value: 10, label: "10 seconds" },
|
||||
{ value: 15, label: "15 seconds" },
|
||||
];
|
||||
|
||||
const syncOptions = [
|
||||
{ value: 0, label: "Off" },
|
||||
{ value: 1, label: "1 minute" },
|
||||
{ value: 2, label: "2 minutes" },
|
||||
{ value: 5, label: "5 minutes" },
|
||||
];
|
||||
|
||||
async function browse() {
|
||||
const selected = await open({ directory: true, multiple: false });
|
||||
if (typeof selected === "string") {
|
||||
@@ -29,7 +52,10 @@
|
||||
app.settings = await api.updateSettings({
|
||||
workspaceRoot,
|
||||
serverUrl,
|
||||
autosaveSeconds,
|
||||
syncMinutes,
|
||||
});
|
||||
restartAutoSync();
|
||||
await refreshEntries();
|
||||
onclose();
|
||||
} catch (error) {
|
||||
@@ -81,6 +107,37 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1 text-xs">
|
||||
<span class="font-medium text-[var(--color-ink-muted)]">Autosave</span>
|
||||
<select
|
||||
class="rounded-md border border-[var(--color-line)] bg-[var(--color-surface)] px-3 py-2 text-sm focus:border-[var(--color-accent)] focus:outline-none"
|
||||
bind:value={autosaveSeconds}
|
||||
>
|
||||
{#each autosaveOptions as option}
|
||||
<option value={option.value}>{option.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<span class="text-[var(--color-ink-muted)]">
|
||||
Saves the file being edited after you stop typing.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1 text-xs">
|
||||
<span class="font-medium text-[var(--color-ink-muted)]">Automatic sync</span>
|
||||
<select
|
||||
class="rounded-md border border-[var(--color-line)] bg-[var(--color-surface)] px-3 py-2 text-sm focus:border-[var(--color-accent)] focus:outline-none"
|
||||
bind:value={syncMinutes}
|
||||
>
|
||||
{#each syncOptions as option}
|
||||
<option value={option.value}>{option.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<span class="text-[var(--color-ink-muted)]">
|
||||
Pulls and pushes cloud-linked projects on a timer. Conflicts pause
|
||||
syncing until they are resolved.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex items-center gap-3 rounded-md border border-[var(--color-line)] bg-[var(--color-surface-muted)] p-3"
|
||||
>
|
||||
|
||||
+83
-1
@@ -6,6 +6,8 @@ export interface Settings {
|
||||
device_token: string | null;
|
||||
account_email: string | null;
|
||||
account_username: string | null;
|
||||
autosave_seconds: number;
|
||||
sync_minutes: number;
|
||||
}
|
||||
|
||||
export interface FileEntry {
|
||||
@@ -143,8 +145,9 @@ export const setTargetEntrypoint = (path: string, entrypoint: string) =>
|
||||
|
||||
export const compileTarget = (
|
||||
path: string,
|
||||
entrypoint?: string,
|
||||
overrides?: Record<string, string>,
|
||||
) => invoke<CompileResult>("compile_target", { path, overrides });
|
||||
) => invoke<CompileResult>("compile_target", { path, entrypoint, overrides });
|
||||
|
||||
export const exportTarget = (
|
||||
path: string,
|
||||
@@ -161,6 +164,19 @@ export interface Asset {
|
||||
|
||||
export const listAssets = () => invoke<Asset[]>("list_assets");
|
||||
|
||||
export interface Resource {
|
||||
name: string;
|
||||
reference: string;
|
||||
path: string;
|
||||
scope: "shared" | "project";
|
||||
kind: "font" | "image" | "file";
|
||||
size: number;
|
||||
font_families: string[];
|
||||
}
|
||||
|
||||
export const listResources = (path: string) =>
|
||||
invoke<Resource[]>("list_resources", { path });
|
||||
|
||||
export interface Thumbnail {
|
||||
kind: "svg" | "image";
|
||||
data: string;
|
||||
@@ -204,11 +220,23 @@ export const importIntoTarget = (path: string, sources: string[]) =>
|
||||
export const importIntoFolder = (parent: string, sources: string[]) =>
|
||||
invoke<string[]>("import_into_folder", { parent, sources });
|
||||
|
||||
export interface AppInfo {
|
||||
version: string;
|
||||
typst_version: string;
|
||||
authors: string;
|
||||
license: string;
|
||||
tauri_version: string;
|
||||
}
|
||||
|
||||
export const appInfo = () => invoke<AppInfo>("app_info");
|
||||
|
||||
export const getSettings = () => invoke<Settings>("get_settings");
|
||||
|
||||
export const updateSettings = (changes: {
|
||||
workspaceRoot?: string;
|
||||
serverUrl?: string;
|
||||
autosaveSeconds?: number;
|
||||
syncMinutes?: number;
|
||||
}) => invoke<Settings>("update_settings", changes);
|
||||
|
||||
export const cloudLogin = (
|
||||
@@ -224,6 +252,60 @@ export const cloudAccount = () => invoke<Account | null>("cloud_account");
|
||||
export const cloudListSpaces = () =>
|
||||
invoke<SpaceSummary[]>("cloud_list_spaces");
|
||||
|
||||
export interface CloudFolder {
|
||||
id: string;
|
||||
name: string;
|
||||
parent_id: string | null;
|
||||
}
|
||||
|
||||
export interface CloudDocument {
|
||||
id: string;
|
||||
title: string;
|
||||
folder_id: string | null;
|
||||
role: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface SharedItems {
|
||||
documents: CloudDocument[];
|
||||
spaces: SpaceSummary[];
|
||||
}
|
||||
|
||||
export interface DocumentLink {
|
||||
document_id: string;
|
||||
base_hash: string;
|
||||
role: string;
|
||||
base_content: string;
|
||||
}
|
||||
|
||||
export const cloudListFolders = () =>
|
||||
invoke<CloudFolder[]>("cloud_list_folders");
|
||||
|
||||
export const cloudListDocuments = (folderId?: string | null) =>
|
||||
invoke<CloudDocument[]>("cloud_list_documents", {
|
||||
folderId: folderId ?? null,
|
||||
});
|
||||
|
||||
export const cloudListShared = () => invoke<SharedItems>("cloud_list_shared");
|
||||
|
||||
export const cloudDownloadDocument = (documentId: string, parent: string) =>
|
||||
invoke<string>("cloud_download_document", { documentId, parent });
|
||||
|
||||
export const cloudSyncDocument = (path: string) =>
|
||||
invoke<SyncReport>("cloud_sync_document", { path });
|
||||
|
||||
export const cloudResolveDocument = (
|
||||
path: string,
|
||||
content: string,
|
||||
serverHash: string,
|
||||
) => invoke<void>("cloud_resolve_document", { path, content, serverHash });
|
||||
|
||||
export const cloudDocumentLink = (path: string) =>
|
||||
invoke<DocumentLink | null>("cloud_document_link", { path });
|
||||
|
||||
export const cloudUnlinkDocument = (path: string) =>
|
||||
invoke<void>("cloud_unlink_document", { path });
|
||||
|
||||
export const cloudCreateSpace = (name: string) =>
|
||||
invoke<SpaceSummary>("cloud_create_space", { name });
|
||||
|
||||
|
||||
+131
-5
@@ -2,9 +2,12 @@ import * as api from "./api";
|
||||
import type {
|
||||
Account,
|
||||
BrowseEntry,
|
||||
CloudDocument,
|
||||
CloudFolder,
|
||||
CompileResult,
|
||||
Conflict,
|
||||
Diagnostic,
|
||||
DocumentLink,
|
||||
Settings,
|
||||
SpaceSummary,
|
||||
TargetInfo,
|
||||
@@ -23,6 +26,11 @@ interface AppState {
|
||||
currentDir: string;
|
||||
entries: BrowseEntry[];
|
||||
spaces: SpaceSummary[];
|
||||
cloudFolder: string | null | "shared";
|
||||
cloudFolders: CloudFolder[];
|
||||
cloudDocuments: CloudDocument[];
|
||||
cloudLoading: boolean;
|
||||
documentLink: DocumentLink | null;
|
||||
|
||||
target: TargetInfo | null;
|
||||
activePath: string | null;
|
||||
@@ -49,6 +57,11 @@ export const app = $state<AppState>({
|
||||
currentDir: "",
|
||||
entries: [],
|
||||
spaces: [],
|
||||
cloudFolder: null,
|
||||
cloudFolders: [],
|
||||
cloudDocuments: [],
|
||||
cloudLoading: false,
|
||||
documentLink: null,
|
||||
|
||||
target: null,
|
||||
activePath: null,
|
||||
@@ -102,6 +115,7 @@ export async function bootstrap() {
|
||||
|
||||
try {
|
||||
app.settings = await api.getSettings();
|
||||
restartAutoSync();
|
||||
await browseTo("");
|
||||
await refreshAccount();
|
||||
} catch (error) {
|
||||
@@ -144,6 +158,53 @@ export async function refreshSpaces() {
|
||||
}
|
||||
}
|
||||
|
||||
export async function refreshCloud() {
|
||||
if (!app.account) return;
|
||||
|
||||
app.cloudLoading = true;
|
||||
try {
|
||||
if (app.cloudFolder === "shared") {
|
||||
const shared = await api.cloudListShared();
|
||||
app.cloudDocuments = shared.documents;
|
||||
app.spaces = shared.spaces;
|
||||
app.cloudFolders = [];
|
||||
} else {
|
||||
const [folders, documents, spaces] = await Promise.all([
|
||||
api.cloudListFolders(),
|
||||
api.cloudListDocuments(app.cloudFolder),
|
||||
api.cloudListSpaces(),
|
||||
]);
|
||||
app.cloudFolders = folders.filter(
|
||||
(folder) => (folder.parent_id ?? null) === app.cloudFolder,
|
||||
);
|
||||
app.cloudDocuments = documents;
|
||||
app.spaces = spaces;
|
||||
}
|
||||
} catch (error) {
|
||||
setError(error);
|
||||
} finally {
|
||||
app.cloudLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function openCloudFolder(id: string | null | "shared") {
|
||||
app.cloudFolder = id;
|
||||
await refreshCloud();
|
||||
}
|
||||
|
||||
export async function downloadDocument(documentId: string, title: string) {
|
||||
try {
|
||||
const path = await api.cloudDownloadDocument(documentId, "");
|
||||
app.scope = "local";
|
||||
await browseTo("");
|
||||
setStatus(`Downloaded '${title}' to this device`);
|
||||
return path;
|
||||
} catch (error) {
|
||||
setError(error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function openTarget(path: string) {
|
||||
try {
|
||||
const target = await api.targetInfo(path);
|
||||
@@ -157,6 +218,10 @@ export async function openTarget(path: string) {
|
||||
app.lspStatus = "off";
|
||||
clearMessages();
|
||||
|
||||
app.documentLink = target.standalone
|
||||
? await api.cloudDocumentLink(path).catch(() => null)
|
||||
: null;
|
||||
|
||||
const preferred =
|
||||
target.files.find((file) => file.path === target.entrypoint) ??
|
||||
target.files.find((file) => file.path.endsWith(".typ")) ??
|
||||
@@ -170,6 +235,7 @@ export async function openTarget(path: string) {
|
||||
|
||||
export async function closeTarget() {
|
||||
cancelScheduledCompile();
|
||||
cancelAutosave();
|
||||
if (app.dirty) await saveActiveFile();
|
||||
app.view = "files";
|
||||
app.target = null;
|
||||
@@ -194,6 +260,7 @@ export async function openFile(file: string) {
|
||||
if (!app.target) return;
|
||||
|
||||
cancelScheduledCompile();
|
||||
cancelAutosave();
|
||||
|
||||
if (app.dirty && app.activePath) await saveActiveFile();
|
||||
|
||||
@@ -242,7 +309,15 @@ export async function compile() {
|
||||
app.compiling = true;
|
||||
|
||||
try {
|
||||
const result = await api.compileTarget(app.target.path, liveOverrides());
|
||||
const previewFile =
|
||||
app.activePath && app.activePath.toLowerCase().endsWith(".typ")
|
||||
? app.activePath
|
||||
: undefined;
|
||||
const result = await api.compileTarget(
|
||||
app.target.path,
|
||||
previewFile,
|
||||
liveOverrides(),
|
||||
);
|
||||
app.compiled = result;
|
||||
app.diagnostics = result.diagnostics;
|
||||
} catch (error) {
|
||||
@@ -280,8 +355,57 @@ export function cancelScheduledCompile() {
|
||||
}
|
||||
}
|
||||
|
||||
let autosaveTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
export function scheduleAutosave() {
|
||||
const seconds = app.settings?.autosave_seconds ?? 0;
|
||||
if (autosaveTimer) clearTimeout(autosaveTimer);
|
||||
if (seconds <= 0) return;
|
||||
|
||||
autosaveTimer = setTimeout(() => {
|
||||
autosaveTimer = null;
|
||||
if (app.dirty) saveActiveFile();
|
||||
}, seconds * 1000);
|
||||
}
|
||||
|
||||
export function cancelAutosave() {
|
||||
if (autosaveTimer) {
|
||||
clearTimeout(autosaveTimer);
|
||||
autosaveTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
let syncTimer: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
export function restartAutoSync() {
|
||||
if (syncTimer) {
|
||||
clearInterval(syncTimer);
|
||||
syncTimer = null;
|
||||
}
|
||||
|
||||
const minutes = app.settings?.sync_minutes ?? 0;
|
||||
if (minutes <= 0) return;
|
||||
|
||||
syncTimer = setInterval(() => {
|
||||
autoSync();
|
||||
}, minutes * 60 * 1000);
|
||||
}
|
||||
|
||||
async function autoSync() {
|
||||
if (!app.account || app.syncing) return;
|
||||
if (app.conflicts.length > 0) return;
|
||||
|
||||
const linked = app.target?.space_id || app.documentLink;
|
||||
const project = linked ? app.target?.path : null;
|
||||
if (!project) return;
|
||||
|
||||
if (app.dirty) await saveActiveFile();
|
||||
await runSync("sync", project, true);
|
||||
}
|
||||
|
||||
export async function saveAndCompile() {
|
||||
cancelScheduledCompile();
|
||||
cancelAutosave();
|
||||
await saveActiveFile();
|
||||
await compile();
|
||||
}
|
||||
@@ -289,15 +413,17 @@ export async function saveAndCompile() {
|
||||
export async function runSync(
|
||||
action: "sync" | "push" | "pull",
|
||||
project = app.target?.path,
|
||||
quiet = false,
|
||||
) {
|
||||
if (!project) return;
|
||||
|
||||
app.syncing = true;
|
||||
clearMessages();
|
||||
if (!quiet) clearMessages();
|
||||
|
||||
try {
|
||||
const report =
|
||||
action === "push"
|
||||
const report = app.documentLink
|
||||
? await api.cloudSyncDocument(project)
|
||||
: action === "push"
|
||||
? await api.cloudPush(project)
|
||||
: action === "pull"
|
||||
? await api.cloudPull(project)
|
||||
@@ -307,7 +433,7 @@ export async function runSync(
|
||||
|
||||
if (report.conflicts.length > 0) {
|
||||
setError(`${report.conflicts.length} file(s) need conflict resolution`);
|
||||
} else {
|
||||
} else if (!quiet) {
|
||||
setStatus(summarize(report));
|
||||
}
|
||||
|
||||
|
||||
+33
-11
@@ -16,6 +16,7 @@
|
||||
import AssetsModal from "$lib/components/AssetsModal.svelte";
|
||||
import WindowControls from "$lib/components/WindowControls.svelte";
|
||||
import ImageViewer from "$lib/components/ImageViewer.svelte";
|
||||
import InfoModal from "$lib/components/InfoModal.svelte";
|
||||
import EditorToolbar from "$lib/components/EditorToolbar.svelte";
|
||||
import PageSettingsModal from "$lib/components/PageSettingsModal.svelte";
|
||||
|
||||
@@ -31,6 +32,7 @@
|
||||
clearMessages,
|
||||
closeTarget,
|
||||
compile,
|
||||
downloadDocument,
|
||||
openFile,
|
||||
openTarget,
|
||||
refreshAccount,
|
||||
@@ -39,6 +41,7 @@
|
||||
refreshTarget,
|
||||
runSync,
|
||||
saveAndCompile,
|
||||
scheduleAutosave,
|
||||
scheduleCompile,
|
||||
setError,
|
||||
setStatus,
|
||||
@@ -61,6 +64,7 @@
|
||||
| { kind: "delete-file"; path: string }
|
||||
| { kind: "login" }
|
||||
| { kind: "settings" }
|
||||
| { kind: "info" }
|
||||
| { kind: "assets" }
|
||||
| { kind: "page-settings" }
|
||||
| { kind: "conflicts" };
|
||||
@@ -231,6 +235,20 @@
|
||||
}
|
||||
}
|
||||
|
||||
const resolveDocumentConflicts = (resolutions: api.Resolution[]) =>
|
||||
guard(async () => {
|
||||
for (const resolution of resolutions) {
|
||||
await api.cloudResolveDocument(
|
||||
app.target!.path,
|
||||
resolution.content,
|
||||
resolution.server_hash,
|
||||
);
|
||||
}
|
||||
app.conflicts = [];
|
||||
if (app.activePath) await openFile(app.activePath);
|
||||
setStatus("Conflicts resolved and uploaded");
|
||||
});
|
||||
|
||||
const resolveConflicts = (resolutions: api.Resolution[]) =>
|
||||
guard(async () => {
|
||||
const report = await api.cloudResolveConflicts(
|
||||
@@ -365,14 +383,6 @@
|
||||
{lspLabel[app.lspStatus]}
|
||||
</span>
|
||||
|
||||
<button
|
||||
class="flex items-center gap-1.5 rounded-md px-2.5 py-1.5 text-xs text-[var(--color-ink-muted)] transition hover:bg-[var(--color-surface-muted)] hover:text-[var(--color-ink)]"
|
||||
onclick={() => (dialog = { kind: "assets" })}
|
||||
>
|
||||
<Icon icon="ph:images" />
|
||||
Assets
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="flex items-center gap-1.5 rounded-md px-2.5 py-1.5 text-xs text-[var(--color-ink-muted)] transition hover:bg-[var(--color-surface-muted)] hover:text-[var(--color-ink)]"
|
||||
onclick={saveAndCompile}
|
||||
@@ -402,7 +412,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if app.target?.space_id}
|
||||
{#if app.target?.space_id || app.documentLink}
|
||||
<button
|
||||
class="flex items-center gap-1.5 rounded-md bg-[var(--color-accent)] px-2.5 py-1.5 text-xs font-medium text-white transition hover:opacity-90 disabled:opacity-50"
|
||||
disabled={app.syncing}
|
||||
@@ -425,6 +435,14 @@
|
||||
<Icon icon="ph:gear-six" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="rounded-md p-1.5 text-[var(--color-ink-muted)] transition hover:bg-[var(--color-surface-muted)] hover:text-[var(--color-ink)]"
|
||||
onclick={() => (dialog = { kind: "info" })}
|
||||
aria-label="About"
|
||||
>
|
||||
<Icon icon="ph:info" />
|
||||
</button>
|
||||
|
||||
<div class="ml-1 h-5 w-px bg-[var(--color-line)]"></div>
|
||||
|
||||
<WindowControls />
|
||||
@@ -461,11 +479,12 @@
|
||||
onnewfolder={() => (dialog = { kind: "new-folder" })}
|
||||
onnewdocument={() => (dialog = { kind: "new-document" })}
|
||||
onupload={importFiles}
|
||||
onassets={() => (dialog = { kind: "assets" })}
|
||||
onrename={(entry) => (dialog = { kind: "rename-entry", entry })}
|
||||
ondelete={(entry) => (dialog = { kind: "delete-entry", entry })}
|
||||
onlink={(entry) => (dialog = { kind: "link-entry", entry })}
|
||||
onviewimage={(paths, index) => (imageViewer = { paths, index })}
|
||||
ondownloaddocument={(documentId, title) =>
|
||||
downloadDocument(documentId, title)}
|
||||
onnewspace={() => (dialog = { kind: "new-space" })}
|
||||
onclonespace={(id, name) => (dialog = { kind: "clone-space", id, name })}
|
||||
ondeletespace={(id) => (dialog = { kind: "delete-space", id })}
|
||||
@@ -553,6 +572,7 @@
|
||||
app.editorContent = value;
|
||||
app.dirty = true;
|
||||
scheduleCompile();
|
||||
scheduleAutosave();
|
||||
}}
|
||||
onsave={saveAndCompile}
|
||||
onlspstatus={(status) => (app.lspStatus = status)}
|
||||
@@ -746,6 +766,8 @@
|
||||
/>
|
||||
{:else if dialog.kind === "settings"}
|
||||
<SettingsModal onclose={close} onsignin={() => (dialog = { kind: "login" })} />
|
||||
{:else if dialog.kind === "info"}
|
||||
<InfoModal onclose={close} />
|
||||
{:else if dialog.kind === "assets"}
|
||||
<AssetsModal
|
||||
oninsert={app.view === "editor" && editorView
|
||||
@@ -762,7 +784,7 @@
|
||||
{:else if dialog.kind === "conflicts"}
|
||||
<ConflictModal
|
||||
conflicts={app.conflicts}
|
||||
onresolve={resolveConflicts}
|
||||
onresolve={app.documentLink ? resolveDocumentConflicts : resolveConflicts}
|
||||
onclose={close}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user