Documents by default, cloud project rename, multi-select drag, new settings
This commit is contained in:
@@ -1,17 +1,20 @@
|
||||
<script lang="ts">
|
||||
import Icon from "@iconify/svelte";
|
||||
import type { FileEntry } from "$lib/ts/api";
|
||||
import { resolveResource } from "@tauri-apps/api/path";
|
||||
import { startDrag } from "@crabnebula/tauri-plugin-drag";
|
||||
import { absolutePath, type FileEntry } from "$lib/ts/api";
|
||||
|
||||
interface Props {
|
||||
files: FileEntry[];
|
||||
activePath: string | null;
|
||||
entrypoint: string;
|
||||
selected: string | null;
|
||||
targetPath: string;
|
||||
selected: Set<string>;
|
||||
dropTarget: string | null;
|
||||
onopen: (path: string) => void;
|
||||
onselect: (path: string | null, isDir: boolean) => void;
|
||||
onselect: (paths: string[], primary: string | null, isDir: boolean) => void;
|
||||
onrename: (path: string) => void;
|
||||
ondelete: (path: string) => void;
|
||||
ondelete: (paths: string[]) => void;
|
||||
onduplicate: (path: string) => void;
|
||||
onreveal: (path: string) => void;
|
||||
onsetentry: (path: string) => void;
|
||||
@@ -25,6 +28,7 @@
|
||||
files,
|
||||
activePath,
|
||||
entrypoint,
|
||||
targetPath,
|
||||
selected,
|
||||
dropTarget,
|
||||
onopen,
|
||||
@@ -111,9 +115,59 @@
|
||||
null,
|
||||
);
|
||||
let dragging = $state<string | null>(null);
|
||||
let dragPaths = $state<string[]>([]);
|
||||
let anchorPath = $state<string | null>(null);
|
||||
let nativeDragSent = false;
|
||||
let iconPathPromise: Promise<string> | null = null;
|
||||
|
||||
function flattenVisible(nodes: TreeNode[]): TreeNode[] {
|
||||
const result: TreeNode[] = [];
|
||||
for (const node of nodes) {
|
||||
result.push(node);
|
||||
if (node.isDir && node.children.length > 0 && !collapsed[node.path]) {
|
||||
result.push(...flattenVisible(node.children));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function selectRange(from: string, to: TreeNode) {
|
||||
const flat = flattenVisible(tree);
|
||||
const fromIndex = flat.findIndex((node) => node.path === from);
|
||||
const toIndex = flat.findIndex((node) => node.path === to.path);
|
||||
|
||||
if (fromIndex === -1 || toIndex === -1) {
|
||||
onselect([to.path], to.path, to.isDir);
|
||||
return;
|
||||
}
|
||||
|
||||
const [start, end] =
|
||||
fromIndex < toIndex ? [fromIndex, toIndex] : [toIndex, fromIndex];
|
||||
const paths = flat.slice(start, end + 1).map((node) => node.path);
|
||||
onselect(paths, to.path, to.isDir);
|
||||
}
|
||||
|
||||
function handleRowClick(event: MouseEvent, node: TreeNode) {
|
||||
if (event.shiftKey && anchorPath) {
|
||||
selectRange(anchorPath, node);
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
const next = new Set(selected);
|
||||
if (next.has(node.path)) {
|
||||
next.delete(node.path);
|
||||
} else {
|
||||
next.add(node.path);
|
||||
}
|
||||
anchorPath = node.path;
|
||||
onselect([...next], node.path, node.isDir);
|
||||
return;
|
||||
}
|
||||
|
||||
anchorPath = node.path;
|
||||
onselect([node.path], node.path, node.isDir);
|
||||
|
||||
function activate(node: TreeNode) {
|
||||
onselect(node.path, node.isDir);
|
||||
if (node.isDir) {
|
||||
collapsed[node.path] = !collapsed[node.path];
|
||||
} else {
|
||||
@@ -123,7 +177,10 @@
|
||||
|
||||
function openMenu(event: MouseEvent, node: TreeNode) {
|
||||
event.preventDefault();
|
||||
onselect(node.path, node.isDir);
|
||||
if (!selected.has(node.path)) {
|
||||
anchorPath = node.path;
|
||||
onselect([node.path], node.path, node.isDir);
|
||||
}
|
||||
menu = { path: node.path, isDir: node.isDir, x: event.clientX, y: event.clientY };
|
||||
}
|
||||
|
||||
@@ -134,7 +191,9 @@
|
||||
}
|
||||
if (event.key === "Delete") {
|
||||
event.preventDefault();
|
||||
ondelete(node.path);
|
||||
const paths =
|
||||
selected.has(node.path) && selected.size > 1 ? [...selected] : [node.path];
|
||||
ondelete(paths);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,6 +216,41 @@
|
||||
const index = path.lastIndexOf("/");
|
||||
return index === -1 ? "" : path.slice(0, index);
|
||||
}
|
||||
|
||||
function joinTargetPath(path: string): string {
|
||||
return targetPath ? `${targetPath}/${path}` : path;
|
||||
}
|
||||
|
||||
function resolveAbsolutePaths(paths: string[]): Promise<string[]> {
|
||||
return Promise.all(paths.map((path) => absolutePath(joinTargetPath(path))));
|
||||
}
|
||||
|
||||
function dragIcon(): Promise<string> {
|
||||
if (!iconPathPromise) iconPathPromise = resolveResource("icons/32x32.png");
|
||||
return iconPathPromise;
|
||||
}
|
||||
|
||||
async function exportViaOsDrag(paths: string[]) {
|
||||
if (nativeDragSent || paths.length === 0) return;
|
||||
nativeDragSent = true;
|
||||
try {
|
||||
const [absolutePaths, icon] = await Promise.all([
|
||||
resolveAbsolutePaths(paths),
|
||||
dragIcon(),
|
||||
]);
|
||||
await startDrag({ item: absolutePaths, icon });
|
||||
} catch (error) {
|
||||
console.error("Failed to start native drag", error);
|
||||
}
|
||||
}
|
||||
|
||||
function handleWindowDragLeave(event: DragEvent) {
|
||||
if (!dragging || nativeDragSent) return;
|
||||
// relatedTarget is null only when the pointer leaves the whole window,
|
||||
// as opposed to moving between elements inside it.
|
||||
if (event.relatedTarget !== null) return;
|
||||
exportViaOsDrag(dragPaths);
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window
|
||||
@@ -164,6 +258,7 @@
|
||||
on:contextmenu={(event) => {
|
||||
if (!(event.target as HTMLElement).closest("[data-tree-row]")) menu = null;
|
||||
}}
|
||||
on:dragleave={handleWindowDragLeave}
|
||||
/>
|
||||
|
||||
{#snippet branch(nodes: TreeNode[], depth: number)}
|
||||
@@ -175,26 +270,35 @@
|
||||
data-tree-dir={node.isDir ? "true" : "false"}
|
||||
role="treeitem"
|
||||
tabindex="0"
|
||||
aria-selected={node.path === selected}
|
||||
aria-selected={selected.has(node.path)}
|
||||
draggable="true"
|
||||
class="group flex items-center gap-1.5 rounded px-2 py-1 text-xs transition
|
||||
{node.path === activePath
|
||||
? 'bg-[var(--color-accent-soft)] text-[var(--color-accent)]'
|
||||
: node.path === selected
|
||||
: selected.has(node.path)
|
||||
? 'bg-[var(--color-surface-sunken)]'
|
||||
: 'text-[var(--color-ink)] hover:bg-[var(--color-surface-sunken)]'}
|
||||
{dropTarget === node.path
|
||||
? 'ring-1 ring-inset ring-[var(--color-accent)]'
|
||||
: ''}"
|
||||
style="padding-left: {depth * 12 + 8}px"
|
||||
onclick={() => activate(node)}
|
||||
onclick={(event) => handleRowClick(event, node)}
|
||||
oncontextmenu={(event) => openMenu(event, node)}
|
||||
onkeydown={(event) => handleKey(event, node)}
|
||||
ondragstart={(event) => {
|
||||
dragging = node.path;
|
||||
dragPaths =
|
||||
selected.has(node.path) && selected.size > 1
|
||||
? [...selected]
|
||||
: [node.path];
|
||||
nativeDragSent = false;
|
||||
event.dataTransfer?.setData("text/plain", node.path);
|
||||
}}
|
||||
ondragend={() => (dragging = null)}
|
||||
ondragend={() => {
|
||||
dragging = null;
|
||||
dragPaths = [];
|
||||
nativeDragSent = false;
|
||||
}}
|
||||
ondragover={(event) => {
|
||||
if (!dragging || !node.isDir) return;
|
||||
event.preventDefault();
|
||||
@@ -254,7 +358,7 @@
|
||||
>
|
||||
<button
|
||||
class="rounded p-1 text-[var(--color-ink-muted)] hover:bg-[var(--color-surface-sunken)] hover:text-[var(--color-ink)]"
|
||||
onclick={() => onnewfile(selected ?? "")}
|
||||
onclick={() => onnewfile(anchorPath ?? "")}
|
||||
title="New file"
|
||||
aria-label="New file"
|
||||
>
|
||||
@@ -262,7 +366,7 @@
|
||||
</button>
|
||||
<button
|
||||
class="rounded p-1 text-[var(--color-ink-muted)] hover:bg-[var(--color-surface-sunken)] hover:text-[var(--color-ink)]"
|
||||
onclick={() => onnewfolder(selected ?? "")}
|
||||
onclick={() => onnewfolder(anchorPath ?? "")}
|
||||
title="New folder"
|
||||
aria-label="New folder"
|
||||
>
|
||||
@@ -270,7 +374,7 @@
|
||||
</button>
|
||||
<button
|
||||
class="rounded p-1 text-[var(--color-ink-muted)] hover:bg-[var(--color-surface-sunken)] hover:text-[var(--color-ink)]"
|
||||
onclick={() => onimport(selected ?? "")}
|
||||
onclick={() => onimport(anchorPath ?? "")}
|
||||
title="Import files"
|
||||
aria-label="Import files"
|
||||
>
|
||||
@@ -293,10 +397,16 @@
|
||||
data-tree-path=""
|
||||
data-tree-dir="true"
|
||||
onclick={(event) => {
|
||||
if (event.target === event.currentTarget) onselect(null, true);
|
||||
if (event.target === event.currentTarget) {
|
||||
anchorPath = null;
|
||||
onselect([], null, true);
|
||||
}
|
||||
}}
|
||||
onkeydown={(event) => {
|
||||
if (event.key === "Escape") onselect(null, true);
|
||||
if (event.key === "Escape") {
|
||||
anchorPath = null;
|
||||
onselect([], null, true);
|
||||
}
|
||||
}}
|
||||
ondragover={(event) => {
|
||||
if (dragging) event.preventDefault();
|
||||
@@ -318,11 +428,14 @@
|
||||
|
||||
{#if menu}
|
||||
{@const target = menu}
|
||||
{@const menuPaths =
|
||||
selected.has(target.path) && selected.size > 1 ? [...selected] : [target.path]}
|
||||
{@const single = menuPaths.length === 1}
|
||||
<div
|
||||
class="fixed z-50 flex w-48 flex-col rounded-md border border-[var(--color-line)] bg-[var(--color-surface)] py-1 text-xs shadow-lg"
|
||||
style="left: {target.x}px; top: {target.y}px"
|
||||
>
|
||||
{#if target.isDir}
|
||||
{#if single && target.isDir}
|
||||
<button
|
||||
class="px-3 py-1.5 text-left hover:bg-[var(--color-surface-sunken)]"
|
||||
onclick={() => onnewfile(target.path)}
|
||||
@@ -342,7 +455,7 @@
|
||||
Import files here
|
||||
</button>
|
||||
<div class="my-1 h-px bg-[var(--color-line)]"></div>
|
||||
{:else if target.path.endsWith(".typ")}
|
||||
{:else if single && target.path.endsWith(".typ")}
|
||||
<button
|
||||
class="px-3 py-1.5 text-left hover:bg-[var(--color-surface-sunken)]"
|
||||
onclick={() => onsetentry(target.path)}
|
||||
@@ -352,36 +465,39 @@
|
||||
<div class="my-1 h-px bg-[var(--color-line)]"></div>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
class="px-3 py-1.5 text-left hover:bg-[var(--color-surface-sunken)]"
|
||||
onclick={() => onrename(target.path)}
|
||||
>
|
||||
Rename
|
||||
</button>
|
||||
<button
|
||||
class="px-3 py-1.5 text-left hover:bg-[var(--color-surface-sunken)]"
|
||||
onclick={() => onduplicate(target.path)}
|
||||
>
|
||||
Duplicate
|
||||
</button>
|
||||
<button
|
||||
class="px-3 py-1.5 text-left hover:bg-[var(--color-surface-sunken)]"
|
||||
onclick={() => navigator.clipboard.writeText(target.path)}
|
||||
>
|
||||
Copy path
|
||||
</button>
|
||||
<button
|
||||
class="px-3 py-1.5 text-left hover:bg-[var(--color-surface-sunken)]"
|
||||
onclick={() => onreveal(target.path)}
|
||||
>
|
||||
Reveal in file manager
|
||||
</button>
|
||||
<div class="my-1 h-px bg-[var(--color-line)]"></div>
|
||||
{#if single}
|
||||
<button
|
||||
class="px-3 py-1.5 text-left hover:bg-[var(--color-surface-sunken)]"
|
||||
onclick={() => onrename(target.path)}
|
||||
>
|
||||
Rename
|
||||
</button>
|
||||
<button
|
||||
class="px-3 py-1.5 text-left hover:bg-[var(--color-surface-sunken)]"
|
||||
onclick={() => onduplicate(target.path)}
|
||||
>
|
||||
Duplicate
|
||||
</button>
|
||||
<button
|
||||
class="px-3 py-1.5 text-left hover:bg-[var(--color-surface-sunken)]"
|
||||
onclick={() => navigator.clipboard.writeText(target.path)}
|
||||
>
|
||||
Copy path
|
||||
</button>
|
||||
<button
|
||||
class="px-3 py-1.5 text-left hover:bg-[var(--color-surface-sunken)]"
|
||||
onclick={() => onreveal(target.path)}
|
||||
>
|
||||
Reveal in file manager
|
||||
</button>
|
||||
<div class="my-1 h-px bg-[var(--color-line)]"></div>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
class="px-3 py-1.5 text-left text-[var(--color-danger)] hover:bg-[var(--color-surface-sunken)]"
|
||||
onclick={() => ondelete(target.path)}
|
||||
onclick={() => ondelete(menuPaths)}
|
||||
>
|
||||
Delete
|
||||
{single ? "Delete" : `Delete ${menuPaths.length} items`}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
browseTo,
|
||||
cloudBreadcrumbs,
|
||||
linkedDocument,
|
||||
linkedSpace,
|
||||
linkedProject,
|
||||
openCloudFolder,
|
||||
openTarget,
|
||||
refreshCloud,
|
||||
@@ -22,13 +22,14 @@
|
||||
onrename: (entry: BrowseEntry) => void;
|
||||
ondelete: (entry: BrowseEntry) => void;
|
||||
onlink: (entry: BrowseEntry) => void;
|
||||
onsavetocloud: (entry: BrowseEntry) => void;
|
||||
onviewimage: (paths: string[], index: number) => void;
|
||||
ondownloaddocument: (documentId: string, title: string) => void;
|
||||
onremovedownload: (path: string) => void;
|
||||
ondownloadfile: (fileId: string, name: string) => void;
|
||||
onclonespace: (spaceId: string, name: string) => void;
|
||||
ondeletespace: (spaceId: string) => void;
|
||||
onnewspace: () => void;
|
||||
oncloneproject: (cloudProjectId: string, name: string) => void;
|
||||
ondeleteproject: (cloudProjectId: string) => void;
|
||||
onnewcloudproject: () => void;
|
||||
onsignin: () => void;
|
||||
}
|
||||
|
||||
@@ -40,13 +41,14 @@
|
||||
onrename,
|
||||
ondelete,
|
||||
onlink,
|
||||
onsavetocloud,
|
||||
onviewimage,
|
||||
ondownloaddocument,
|
||||
onremovedownload,
|
||||
ondownloadfile,
|
||||
onclonespace,
|
||||
ondeletespace,
|
||||
onnewspace,
|
||||
oncloneproject,
|
||||
ondeleteproject,
|
||||
onnewcloudproject,
|
||||
onsignin,
|
||||
}: Props = $props();
|
||||
|
||||
@@ -82,7 +84,7 @@
|
||||
|
||||
const pending = [
|
||||
...app.linkedDocuments.map((linked) => linked.path),
|
||||
...app.linkedSpaces.map((linked) => linked.path),
|
||||
...app.linkedProjects.map((linked) => linked.path),
|
||||
];
|
||||
let cancelled = false;
|
||||
|
||||
@@ -353,7 +355,7 @@
|
||||
Open image
|
||||
</button>
|
||||
{/if}
|
||||
{#if entry.kind === "project" && !entry.space_id && app.account}
|
||||
{#if entry.kind === "project" && !entry.cloud_project_id && app.account}
|
||||
<button
|
||||
class="px-3 py-1.5 text-left hover:bg-[var(--color-surface-sunken)]"
|
||||
onclick={() => {
|
||||
@@ -364,6 +366,17 @@
|
||||
Upload to cloud
|
||||
</button>
|
||||
{/if}
|
||||
{#if entry.kind === "document" && !entry.cloud_linked && app.account}
|
||||
<button
|
||||
class="px-3 py-1.5 text-left hover:bg-[var(--color-surface-sunken)]"
|
||||
onclick={() => {
|
||||
onsavetocloud(entry);
|
||||
menuFor = null;
|
||||
}}
|
||||
>
|
||||
Save to cloud
|
||||
</button>
|
||||
{/if}
|
||||
<button
|
||||
class="px-3 py-1.5 text-left hover:bg-[var(--color-surface-sunken)]"
|
||||
onclick={() => {
|
||||
@@ -432,25 +445,25 @@
|
||||
</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={onnewdocument}
|
||||
onclick={onnewproject}
|
||||
>
|
||||
<Icon icon="ph:file-plus" />
|
||||
Document
|
||||
<Icon icon="ph:folder-star" />
|
||||
Project
|
||||
</button>
|
||||
<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"
|
||||
onclick={onnewproject}
|
||||
onclick={onnewdocument}
|
||||
>
|
||||
<Icon icon="ph:plus" />
|
||||
New project
|
||||
New document
|
||||
</button>
|
||||
{:else if app.account}
|
||||
<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"
|
||||
onclick={onnewspace}
|
||||
onclick={onnewcloudproject}
|
||||
>
|
||||
<Icon icon="ph:plus" />
|
||||
New space
|
||||
New cloud project
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -490,16 +503,16 @@
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
class="rounded-md bg-[var(--color-accent)] px-3 py-1.5 text-xs font-medium text-white hover:opacity-90"
|
||||
onclick={onnewproject}
|
||||
>
|
||||
New project
|
||||
</button>
|
||||
<button
|
||||
class="rounded-md border border-[var(--color-line)] px-3 py-1.5 text-xs hover:bg-[var(--color-surface)]"
|
||||
onclick={onnewdocument}
|
||||
>
|
||||
New document
|
||||
</button>
|
||||
<button
|
||||
class="rounded-md border border-[var(--color-line)] px-3 py-1.5 text-xs hover:bg-[var(--color-surface)]"
|
||||
onclick={onnewproject}
|
||||
>
|
||||
New project
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
@@ -798,7 +811,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if app.spaces.length === 0 && app.cloudDocuments.length === 0 && app.cloudFolders.length === 0 && app.cloudFiles.length === 0}
|
||||
{#if app.cloudProjects.length === 0 && app.cloudDocuments.length === 0 && app.cloudFolders.length === 0 && app.cloudFiles.length === 0}
|
||||
<div
|
||||
class="flex flex-col items-center justify-center gap-3 py-16 text-[var(--color-ink-muted)]"
|
||||
>
|
||||
@@ -807,26 +820,26 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if app.spaces.length > 0}
|
||||
{#if app.cloudProjects.length > 0}
|
||||
<h2
|
||||
class="mb-2 text-[10px] font-semibold uppercase tracking-wider text-[var(--color-ink-muted)]"
|
||||
>
|
||||
Spaces
|
||||
Cloud projects
|
||||
</h2>
|
||||
{/if}
|
||||
<div class="grid grid-cols-[repeat(auto-fill,minmax(220px,1fr))] gap-3">
|
||||
{#each app.spaces as space (space.id)}
|
||||
{@const linked = linkedSpace(space.id)}
|
||||
{#each app.cloudProjects as project (project.id)}
|
||||
{@const linked = linkedProject(project.id)}
|
||||
{@render cloudCard(
|
||||
"ph:folder-star",
|
||||
space.name,
|
||||
project.name,
|
||||
linked
|
||||
? `Project · ${space.role}`
|
||||
: `${space.role} · ${formatDate(space.updated_at)}`,
|
||||
? `Project · ${project.role}`
|
||||
: `${project.role} · ${formatDate(project.updated_at)}`,
|
||||
linked,
|
||||
linked ? () => openTarget(linked.path) : null,
|
||||
() => onclonespace(space.id, space.name),
|
||||
space.role === "owner" ? () => ondeletespace(space.id) : null,
|
||||
() => oncloneproject(project.id, project.name),
|
||||
project.role === "owner" ? () => ondeleteproject(project.id) : null,
|
||||
)}
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -9,9 +9,15 @@
|
||||
import {
|
||||
app,
|
||||
applyTheme,
|
||||
applyAccent,
|
||||
applyTextScale,
|
||||
applyReduceMotion,
|
||||
applyContrast,
|
||||
refreshEntries,
|
||||
restartAutoSync,
|
||||
setError,
|
||||
type ThemePreference,
|
||||
type TextScale,
|
||||
} from "$lib/ts/state.svelte";
|
||||
|
||||
interface Props {
|
||||
@@ -21,15 +27,37 @@
|
||||
|
||||
let { onclose, onsignin }: Props = $props();
|
||||
|
||||
type Section = "files" | "appearance" | "account" | "about";
|
||||
type Section =
|
||||
| "files"
|
||||
| "appearance"
|
||||
| "accessibility"
|
||||
| "account"
|
||||
| "about";
|
||||
|
||||
const sections: { id: Section; label: string; icon: string }[] = [
|
||||
{ id: "files", label: "Files", icon: "ph:folder" },
|
||||
{ id: "appearance", label: "Appearance", icon: "ph:palette" },
|
||||
{ id: "accessibility", label: "Accessibility", icon: "ph:wheelchair" },
|
||||
{ id: "account", label: "Account", icon: "ph:user-circle" },
|
||||
{ id: "about", label: "About", icon: "ph:info" },
|
||||
];
|
||||
|
||||
const accentPresets = [
|
||||
"#3b6cf6",
|
||||
"#7c5cfc",
|
||||
"#22b573",
|
||||
"#f2994a",
|
||||
"#ec4899",
|
||||
"#14b8a6",
|
||||
];
|
||||
|
||||
const textScaleOptions: { value: TextScale; label: string }[] = [
|
||||
{ value: "small", label: "Small" },
|
||||
{ value: "default", label: "Default" },
|
||||
{ value: "large", label: "Large" },
|
||||
{ value: "xlarge", label: "Extra large" },
|
||||
];
|
||||
|
||||
let section = $state<Section>("files");
|
||||
|
||||
let workspaceRoot = $state(untrack(() => app.settings?.workspace_root ?? ""));
|
||||
@@ -105,7 +133,7 @@
|
||||
try {
|
||||
await api.cloudLogout();
|
||||
app.account = null;
|
||||
app.spaces = [];
|
||||
app.cloudProjects = [];
|
||||
app.settings = await api.getSettings();
|
||||
} catch (error) {
|
||||
setError(error);
|
||||
@@ -172,13 +200,13 @@
|
||||
<div class="flex flex-col gap-2 text-xs">
|
||||
<span class="font-medium text-[var(--color-ink-muted)]">Theme</span>
|
||||
<div class="flex gap-2">
|
||||
{#each [["light", "Light", "ph:sun"], ["dark", "Dark", "ph:moon"]] as [value, label, icon]}
|
||||
{#each [["light", "Light", "ph:sun"], ["dark", "Dark", "ph:moon"], ["system", "System", "ph:desktop"]] as [value, label, icon]}
|
||||
<button
|
||||
class="flex flex-1 items-center justify-center gap-1.5 rounded-md border px-3 py-2.5 transition
|
||||
{app.theme === value
|
||||
{app.themePreference === value
|
||||
? 'border-[var(--color-accent)] bg-[var(--color-accent-soft)] text-[var(--color-accent)]'
|
||||
: 'border-[var(--color-line)] text-[var(--color-ink-muted)] hover:bg-[var(--color-surface-muted)]'}"
|
||||
onclick={() => applyTheme(value as "light" | "dark")}
|
||||
onclick={() => applyTheme(value as ThemePreference)}
|
||||
>
|
||||
<Icon {icon} class="text-base" />
|
||||
{label}
|
||||
@@ -186,7 +214,111 @@
|
||||
{/each}
|
||||
</div>
|
||||
<span class="text-[var(--color-ink-muted)]">
|
||||
Applies immediately to the editor and preview.
|
||||
System follows your OS setting and updates live.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2 text-xs">
|
||||
<span class="font-medium text-[var(--color-ink-muted)]">
|
||||
Accent color
|
||||
</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
class="flex h-7 w-7 items-center justify-center rounded-full border transition
|
||||
{app.accent === null
|
||||
? 'border-[var(--color-accent)] text-[var(--color-accent)]'
|
||||
: 'border-[var(--color-line)] text-[var(--color-ink-muted)] hover:bg-[var(--color-surface-muted)]'}"
|
||||
title="Default"
|
||||
onclick={() => applyAccent(null)}
|
||||
>
|
||||
<Icon icon="ph:arrow-counter-clockwise" class="text-sm" />
|
||||
</button>
|
||||
{#each accentPresets as preset}
|
||||
<button
|
||||
class="h-7 w-7 rounded-full border-2 transition
|
||||
{app.accent === preset
|
||||
? 'border-[var(--color-ink)]'
|
||||
: 'border-transparent hover:opacity-80'}"
|
||||
style="background-color: {preset}"
|
||||
title={preset}
|
||||
onclick={() => applyAccent(preset)}
|
||||
></button>
|
||||
{/each}
|
||||
<input
|
||||
type="color"
|
||||
class="h-7 w-7 cursor-pointer rounded-full border border-[var(--color-line)] bg-transparent p-0"
|
||||
value={app.accent ?? "#3b6cf6"}
|
||||
title="Custom color"
|
||||
oninput={(event) =>
|
||||
applyAccent((event.target as HTMLInputElement).value)}
|
||||
/>
|
||||
</div>
|
||||
<span class="text-[var(--color-ink-muted)]">
|
||||
Overrides the accent color used across the app.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{:else if section === "accessibility"}
|
||||
<div class="flex flex-col gap-5">
|
||||
<div class="flex flex-col gap-2 text-xs">
|
||||
<span class="font-medium text-[var(--color-ink-muted)]">
|
||||
UI text scale
|
||||
</span>
|
||||
<div class="flex gap-2">
|
||||
{#each textScaleOptions as option}
|
||||
<button
|
||||
class="flex flex-1 items-center justify-center rounded-md border px-3 py-2.5 transition
|
||||
{app.textScale === option.value
|
||||
? 'border-[var(--color-accent)] bg-[var(--color-accent-soft)] text-[var(--color-accent)]'
|
||||
: 'border-[var(--color-line)] text-[var(--color-ink-muted)] hover:bg-[var(--color-surface-muted)]'}"
|
||||
onclick={() => applyTextScale(option.value)}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
<span class="text-[var(--color-ink-muted)]">
|
||||
Scales text and controls throughout the app.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2 text-xs">
|
||||
<label class="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={app.reduceMotion}
|
||||
onchange={(event) =>
|
||||
applyReduceMotion(
|
||||
(event.target as HTMLInputElement).checked,
|
||||
)}
|
||||
/>
|
||||
<span class="font-medium text-[var(--color-ink-muted)]">
|
||||
Reduce motion
|
||||
</span>
|
||||
</label>
|
||||
<span class="text-[var(--color-ink-muted)]">
|
||||
Shortens transitions and animations across the app.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2 text-xs">
|
||||
<label class="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={app.contrast === "high"}
|
||||
onchange={(event) =>
|
||||
applyContrast(
|
||||
(event.target as HTMLInputElement).checked
|
||||
? "high"
|
||||
: "normal",
|
||||
)}
|
||||
/>
|
||||
<span class="font-medium text-[var(--color-ink-muted)]">
|
||||
High contrast
|
||||
</span>
|
||||
</label>
|
||||
<span class="text-[var(--color-ink-muted)]">
|
||||
Increases contrast for borders, muted text, and focus outlines.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -305,13 +437,17 @@
|
||||
</div>
|
||||
|
||||
{#snippet footer()}
|
||||
{@const draftless =
|
||||
section === "about" ||
|
||||
section === "appearance" ||
|
||||
section === "accessibility"}
|
||||
<button
|
||||
class="rounded-md px-3 py-1.5 text-xs text-[var(--color-ink-muted)] hover:bg-[var(--color-surface-sunken)]"
|
||||
onclick={onclose}
|
||||
>
|
||||
{section === "about" || section === "appearance" ? "Close" : "Cancel"}
|
||||
{draftless ? "Close" : "Cancel"}
|
||||
</button>
|
||||
{#if section !== "about" && section !== "appearance"}
|
||||
{#if !draftless}
|
||||
<button
|
||||
class="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={saving}
|
||||
|
||||
+24
-18
@@ -53,7 +53,7 @@ export interface Account {
|
||||
email: string;
|
||||
}
|
||||
|
||||
export interface SpaceSummary {
|
||||
export interface ProjectSummary {
|
||||
id: string;
|
||||
name: string;
|
||||
entrypoint: string;
|
||||
@@ -94,7 +94,7 @@ export interface BrowseEntry {
|
||||
kind: EntryKind;
|
||||
size: number;
|
||||
modified: string | null;
|
||||
space_id: string | null;
|
||||
cloud_project_id: string | null;
|
||||
last_synced_at: string | null;
|
||||
child_count: number;
|
||||
cloud_linked: boolean;
|
||||
@@ -106,7 +106,7 @@ export interface TargetInfo {
|
||||
entrypoint: string;
|
||||
standalone: boolean;
|
||||
is_project: boolean;
|
||||
space_id: string | null;
|
||||
cloud_project_id: string | null;
|
||||
files: FileEntry[];
|
||||
}
|
||||
|
||||
@@ -261,8 +261,8 @@ export const cloudLogout = () => invoke<void>("cloud_logout");
|
||||
|
||||
export const cloudAccount = () => invoke<Account | null>("cloud_account");
|
||||
|
||||
export const cloudListSpaces = () =>
|
||||
invoke<SpaceSummary[]>("cloud_list_spaces");
|
||||
export const cloudListProjects = () =>
|
||||
invoke<ProjectSummary[]>("cloud_list_projects");
|
||||
|
||||
export interface CloudFolder {
|
||||
id: string;
|
||||
@@ -280,7 +280,7 @@ export interface CloudDocument {
|
||||
|
||||
export interface SharedItems {
|
||||
documents: CloudDocument[];
|
||||
spaces: SpaceSummary[];
|
||||
projects: ProjectSummary[];
|
||||
}
|
||||
|
||||
export interface DocumentLink {
|
||||
@@ -317,6 +317,9 @@ export const cloudDownloadFile = (fileId: string) =>
|
||||
export const cloudDownloadDocument = (documentId: string, parent: string) =>
|
||||
invoke<string>("cloud_download_document", { documentId, parent });
|
||||
|
||||
export const cloudCreateDocument = (path: string, title: string) =>
|
||||
invoke<string>("cloud_create_document", { path, title });
|
||||
|
||||
export const cloudSyncDocument = (path: string) =>
|
||||
invoke<SyncReport>("cloud_sync_document", { path });
|
||||
|
||||
@@ -333,9 +336,9 @@ export interface LinkedDocument {
|
||||
sync_state: "synced" | "pending" | null;
|
||||
}
|
||||
|
||||
export interface LinkedSpace {
|
||||
export interface LinkedProject {
|
||||
path: string;
|
||||
space_id: string;
|
||||
cloud_project_id: string;
|
||||
synced_at: string | null;
|
||||
sync_state: "synced" | "pending" | null;
|
||||
}
|
||||
@@ -343,8 +346,8 @@ export interface LinkedSpace {
|
||||
export const cloudLinkedDocuments = () =>
|
||||
invoke<LinkedDocument[]>("cloud_linked_documents");
|
||||
|
||||
export const cloudLinkedSpaces = () =>
|
||||
invoke<LinkedSpace[]>("cloud_linked_spaces");
|
||||
export const cloudLinkedProjects = () =>
|
||||
invoke<LinkedProject[]>("cloud_linked_projects");
|
||||
|
||||
export const cloudDocumentLink = (path: string) =>
|
||||
invoke<DocumentLink | null>("cloud_document_link", { path });
|
||||
@@ -352,17 +355,20 @@ export const cloudDocumentLink = (path: string) =>
|
||||
export const cloudUnlinkDocument = (path: string) =>
|
||||
invoke<void>("cloud_unlink_document", { path });
|
||||
|
||||
export const cloudCreateSpace = (name: string) =>
|
||||
invoke<SpaceSummary>("cloud_create_space", { name });
|
||||
export const cloudCreateProject = (name: string) =>
|
||||
invoke<ProjectSummary>("cloud_create_project", { name });
|
||||
|
||||
export const cloudDeleteSpace = (spaceId: string) =>
|
||||
invoke<void>("cloud_delete_space", { spaceId });
|
||||
export const cloudDeleteProject = (cloudProjectId: string) =>
|
||||
invoke<void>("cloud_delete_project", { cloudProjectId });
|
||||
|
||||
export const cloudCloneSpace = (spaceId: string, projectName: string) =>
|
||||
invoke<SyncReport>("cloud_clone_space", { spaceId, projectName });
|
||||
export const cloudCloneProject = (cloudProjectId: string, projectName: string) =>
|
||||
invoke<SyncReport>("cloud_clone_project", { cloudProjectId, projectName });
|
||||
|
||||
export const cloudLinkProject = (project: string, spaceId?: string) =>
|
||||
invoke<SyncReport>("cloud_link_project", { project, spaceId: spaceId ?? null });
|
||||
export const cloudLinkProject = (project: string, cloudProjectId?: string) =>
|
||||
invoke<SyncReport>("cloud_link_project", {
|
||||
project,
|
||||
cloudProjectId: cloudProjectId ?? null,
|
||||
});
|
||||
|
||||
export const cloudUnlinkProject = (project: string) =>
|
||||
invoke<void>("cloud_unlink_project", { project });
|
||||
|
||||
+186
-24
@@ -10,15 +10,18 @@ import type {
|
||||
Diagnostic,
|
||||
DocumentLink,
|
||||
LinkedDocument,
|
||||
LinkedSpace,
|
||||
LinkedProject,
|
||||
ProjectSummary,
|
||||
Settings,
|
||||
SpaceSummary,
|
||||
TargetInfo,
|
||||
} from "./api";
|
||||
|
||||
export type Scope = "local" | "cloud";
|
||||
export type View = "files" | "editor";
|
||||
export type LspStatus = "off" | "starting" | "on" | "unavailable";
|
||||
export type ThemePreference = "light" | "dark" | "system";
|
||||
export type TextScale = "small" | "default" | "large" | "xlarge";
|
||||
export type ContrastLevel = "normal" | "high";
|
||||
|
||||
interface AppState {
|
||||
view: View;
|
||||
@@ -28,7 +31,7 @@ interface AppState {
|
||||
|
||||
currentDir: string;
|
||||
entries: BrowseEntry[];
|
||||
spaces: SpaceSummary[];
|
||||
cloudProjects: ProjectSummary[];
|
||||
cloudFolder: string | null | "shared";
|
||||
cloudFolders: CloudFolder[];
|
||||
cloudFolderTree: CloudFolder[];
|
||||
@@ -36,7 +39,7 @@ interface AppState {
|
||||
cloudFiles: CloudFile[];
|
||||
cloudLoading: boolean;
|
||||
linkedDocuments: LinkedDocument[];
|
||||
linkedSpaces: LinkedSpace[];
|
||||
linkedProjects: LinkedProject[];
|
||||
documentLink: DocumentLink | null;
|
||||
|
||||
target: TargetInfo | null;
|
||||
@@ -54,6 +57,11 @@ interface AppState {
|
||||
status: string;
|
||||
error: string;
|
||||
theme: "light" | "dark";
|
||||
themePreference: ThemePreference;
|
||||
accent: string | null;
|
||||
textScale: TextScale;
|
||||
reduceMotion: boolean;
|
||||
contrast: ContrastLevel;
|
||||
}
|
||||
|
||||
export const app = $state<AppState>({
|
||||
@@ -64,7 +72,7 @@ export const app = $state<AppState>({
|
||||
|
||||
currentDir: "",
|
||||
entries: [],
|
||||
spaces: [],
|
||||
cloudProjects: [],
|
||||
cloudFolder: null,
|
||||
cloudFolders: [],
|
||||
cloudFolderTree: [],
|
||||
@@ -72,7 +80,7 @@ export const app = $state<AppState>({
|
||||
cloudFiles: [],
|
||||
cloudLoading: false,
|
||||
linkedDocuments: [],
|
||||
linkedSpaces: [],
|
||||
linkedProjects: [],
|
||||
documentLink: null,
|
||||
|
||||
target: null,
|
||||
@@ -90,6 +98,11 @@ export const app = $state<AppState>({
|
||||
status: "",
|
||||
error: "",
|
||||
theme: "light",
|
||||
themePreference: "light",
|
||||
accent: null,
|
||||
textScale: "default",
|
||||
reduceMotion: false,
|
||||
contrast: "normal",
|
||||
});
|
||||
|
||||
export interface DownloadProgress {
|
||||
@@ -132,10 +145,138 @@ export function clearMessages() {
|
||||
app.error = "";
|
||||
}
|
||||
|
||||
export function applyTheme(theme: "light" | "dark") {
|
||||
app.theme = theme;
|
||||
document.documentElement.dataset.theme = theme;
|
||||
localStorage.setItem("typst-desktop-theme", theme);
|
||||
const THEME_KEY = "typst-desktop-theme";
|
||||
const ACCENT_KEY = "typst-desktop-accent";
|
||||
const TEXT_SCALE_KEY = "typst-desktop-text-scale";
|
||||
const REDUCE_MOTION_KEY = "typst-desktop-reduce-motion";
|
||||
const CONTRAST_KEY = "typst-desktop-contrast";
|
||||
|
||||
const TEXT_SCALE_PX: Record<TextScale, number> = {
|
||||
small: 14,
|
||||
default: 16,
|
||||
large: 18,
|
||||
xlarge: 20,
|
||||
};
|
||||
|
||||
let systemThemeQuery: MediaQueryList | null = null;
|
||||
|
||||
function resolveTheme(preference: ThemePreference): "light" | "dark" {
|
||||
if (preference !== "system") return preference;
|
||||
|
||||
if (!systemThemeQuery) {
|
||||
systemThemeQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
systemThemeQuery.addEventListener("change", () => {
|
||||
if (app.themePreference === "system") applyTheme("system");
|
||||
});
|
||||
}
|
||||
|
||||
return systemThemeQuery.matches ? "dark" : "light";
|
||||
}
|
||||
|
||||
export function applyTheme(preference: ThemePreference) {
|
||||
app.themePreference = preference;
|
||||
app.theme = resolveTheme(preference);
|
||||
document.documentElement.dataset.theme = app.theme;
|
||||
localStorage.setItem(THEME_KEY, preference);
|
||||
|
||||
if (app.accent) applyAccent(app.accent);
|
||||
}
|
||||
|
||||
function hexToRgb(hex: string): [number, number, number] {
|
||||
const value = parseInt(hex.replace("#", ""), 16);
|
||||
return [(value >> 16) & 255, (value >> 8) & 255, value & 255];
|
||||
}
|
||||
|
||||
function rgbToHsl(r: number, g: number, b: number): [number, number, number] {
|
||||
r /= 255;
|
||||
g /= 255;
|
||||
b /= 255;
|
||||
const max = Math.max(r, g, b);
|
||||
const min = Math.min(r, g, b);
|
||||
const l = (max + min) / 2;
|
||||
let h = 0;
|
||||
let s = 0;
|
||||
|
||||
if (max !== min) {
|
||||
const d = max - min;
|
||||
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
||||
switch (max) {
|
||||
case r:
|
||||
h = (g - b) / d + (g < b ? 6 : 0);
|
||||
break;
|
||||
case g:
|
||||
h = (b - r) / d + 2;
|
||||
break;
|
||||
default:
|
||||
h = (r - g) / d + 4;
|
||||
}
|
||||
h /= 6;
|
||||
}
|
||||
|
||||
return [h * 360, s * 100, l * 100];
|
||||
}
|
||||
|
||||
function hslToHex(h: number, s: number, l: number): string {
|
||||
s /= 100;
|
||||
l /= 100;
|
||||
const k = (n: number) => (n + h / 30) % 12;
|
||||
const a = s * Math.min(l, 1 - l);
|
||||
const f = (n: number) =>
|
||||
l - a * Math.max(-1, Math.min(k(n) - 3, 9 - k(n), 1));
|
||||
const toHex = (n: number) =>
|
||||
Math.round(n * 255)
|
||||
.toString(16)
|
||||
.padStart(2, "0");
|
||||
return `#${toHex(f(0))}${toHex(f(8))}${toHex(f(4))}`;
|
||||
}
|
||||
|
||||
function accentSoft(hex: string, dark: boolean): string {
|
||||
const [r, g, b] = hexToRgb(hex);
|
||||
const [h, s] = rgbToHsl(r, g, b);
|
||||
return dark
|
||||
? hslToHex(h, Math.min(s, 55), 18)
|
||||
: hslToHex(h, Math.min(s, 70), 92);
|
||||
}
|
||||
|
||||
export function applyAccent(color: string | null) {
|
||||
app.accent = color;
|
||||
const root = document.documentElement.style;
|
||||
|
||||
if (color) {
|
||||
root.setProperty("--color-accent", color);
|
||||
root.setProperty("--color-accent-soft", accentSoft(color, app.theme === "dark"));
|
||||
localStorage.setItem(ACCENT_KEY, color);
|
||||
} else {
|
||||
root.removeProperty("--color-accent");
|
||||
root.removeProperty("--color-accent-soft");
|
||||
localStorage.removeItem(ACCENT_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
export function applyTextScale(scale: TextScale) {
|
||||
app.textScale = scale;
|
||||
document.documentElement.style.fontSize = `${TEXT_SCALE_PX[scale]}px`;
|
||||
localStorage.setItem(TEXT_SCALE_KEY, scale);
|
||||
}
|
||||
|
||||
export function applyReduceMotion(enabled: boolean) {
|
||||
app.reduceMotion = enabled;
|
||||
if (enabled) {
|
||||
document.documentElement.dataset.reduceMotion = "true";
|
||||
} else {
|
||||
delete document.documentElement.dataset.reduceMotion;
|
||||
}
|
||||
localStorage.setItem(REDUCE_MOTION_KEY, String(enabled));
|
||||
}
|
||||
|
||||
export function applyContrast(level: ContrastLevel) {
|
||||
app.contrast = level;
|
||||
if (level === "high") {
|
||||
document.documentElement.dataset.contrast = "high";
|
||||
} else {
|
||||
delete document.documentElement.dataset.contrast;
|
||||
}
|
||||
localStorage.setItem(CONTRAST_KEY, level);
|
||||
}
|
||||
|
||||
export function breadcrumbs(): { name: string; path: string }[] {
|
||||
@@ -148,8 +289,27 @@ export function breadcrumbs(): { name: string; path: string }[] {
|
||||
}
|
||||
|
||||
export async function bootstrap() {
|
||||
const stored = localStorage.getItem("typst-desktop-theme");
|
||||
applyTheme(stored === "dark" ? "dark" : "light");
|
||||
const storedTheme = localStorage.getItem(THEME_KEY);
|
||||
applyTheme(
|
||||
storedTheme === "dark" || storedTheme === "light" || storedTheme === "system"
|
||||
? storedTheme
|
||||
: "light",
|
||||
);
|
||||
|
||||
const storedAccent = localStorage.getItem(ACCENT_KEY);
|
||||
if (storedAccent) applyAccent(storedAccent);
|
||||
|
||||
const storedTextScale = localStorage.getItem(TEXT_SCALE_KEY);
|
||||
applyTextScale(
|
||||
storedTextScale === "small" ||
|
||||
storedTextScale === "large" ||
|
||||
storedTextScale === "xlarge"
|
||||
? storedTextScale
|
||||
: "default",
|
||||
);
|
||||
|
||||
applyReduceMotion(localStorage.getItem(REDUCE_MOTION_KEY) === "true");
|
||||
applyContrast(localStorage.getItem(CONTRAST_KEY) === "high" ? "high" : "normal");
|
||||
|
||||
try {
|
||||
app.settings = await api.getSettings();
|
||||
@@ -179,18 +339,18 @@ export async function refreshAccount() {
|
||||
try {
|
||||
app.account = await api.cloudAccount();
|
||||
if (app.account) {
|
||||
await refreshSpaces();
|
||||
await refreshCloudProjects();
|
||||
} else {
|
||||
app.spaces = [];
|
||||
app.cloudProjects = [];
|
||||
}
|
||||
} catch {
|
||||
app.account = null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function refreshSpaces() {
|
||||
export async function refreshCloudProjects() {
|
||||
try {
|
||||
app.spaces = await api.cloudListSpaces();
|
||||
app.cloudProjects = await api.cloudListProjects();
|
||||
} catch (error) {
|
||||
setError(error);
|
||||
}
|
||||
@@ -202,19 +362,19 @@ export async function refreshCloud() {
|
||||
app.cloudLoading = true;
|
||||
try {
|
||||
app.linkedDocuments = await api.cloudLinkedDocuments().catch(() => []);
|
||||
app.linkedSpaces = await api.cloudLinkedSpaces().catch(() => []);
|
||||
app.linkedProjects = await api.cloudLinkedProjects().catch(() => []);
|
||||
|
||||
if (app.cloudFolder === "shared") {
|
||||
const shared = await api.cloudListShared();
|
||||
app.cloudDocuments = shared.documents;
|
||||
app.spaces = shared.spaces;
|
||||
app.cloudProjects = shared.projects;
|
||||
app.cloudFolders = [];
|
||||
app.cloudFiles = [];
|
||||
} else {
|
||||
const [folders, documents, spaces, files] = await Promise.all([
|
||||
const [folders, documents, projects, files] = await Promise.all([
|
||||
api.cloudListFolders(),
|
||||
api.cloudListDocuments(app.cloudFolder),
|
||||
api.cloudListSpaces(),
|
||||
api.cloudListProjects(),
|
||||
api.cloudListFiles(app.cloudFolder),
|
||||
]);
|
||||
app.cloudFolderTree = folders;
|
||||
@@ -222,7 +382,7 @@ export async function refreshCloud() {
|
||||
(folder) => (folder.parent_id ?? null) === app.cloudFolder,
|
||||
);
|
||||
app.cloudDocuments = documents;
|
||||
app.spaces = spaces;
|
||||
app.cloudProjects = projects;
|
||||
app.cloudFiles = files;
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -279,8 +439,10 @@ export async function downloadCloudFile(fileId: string, name: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export function linkedSpace(spaceId: string) {
|
||||
return app.linkedSpaces.find((linked) => linked.space_id === spaceId);
|
||||
export function linkedProject(cloudProjectId: string) {
|
||||
return app.linkedProjects.find(
|
||||
(linked) => linked.cloud_project_id === cloudProjectId,
|
||||
);
|
||||
}
|
||||
|
||||
export async function removeDownloadedDocument(path: string) {
|
||||
@@ -484,7 +646,7 @@ async function autoSync() {
|
||||
if (!app.account || app.syncing) return;
|
||||
if (app.conflicts.length > 0) return;
|
||||
|
||||
const linked = app.target?.space_id || app.documentLink;
|
||||
const linked = app.target?.cloud_project_id || app.documentLink;
|
||||
const project = linked ? app.target?.path : null;
|
||||
if (!project) return;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user