This commit is contained in:
vcoppe committed 2026-10-16 18:14:59 +02:00
1 parent 005cc7b305
commit b0a3c3a9ba
18 files changed
+1721 -714

No files matched your search

+92 -45
View File
@@ -4,8 +4,7 @@ import { get } from 'svelte/store';
import { boundsManager } from '$lib/state/bounds';
import { engine } from '$lib/engine';
import { defaultFileName } from '$lib/actions/default-file-name';
import { askUnsaved } from '$lib/components/file-list/unsaved-prompt.svelte';
import { subfolder } from '$lib/engine/workspace';
import { askOverwrite, askUnsaved } from '$lib/components/file-list/unsaved-prompt.svelte';
import type { UnsavedChoice } from '$lib/engine';
/** The name of a new file: the translated default name, numbered if other files have it. */
@@ -32,71 +31,119 @@ export async function openFiles() {
}
}
/**
* What to do with the files of the open folder that have unsaved changes, which the user is asked
* when there are some: `undefined` when there are none, `'cancel'` when they do not want to go on.
*/
async function askAboutUnsavedFiles(): Promise<UnsavedChoice | undefined | 'cancel'> {
const workspace = get(engine.workspace);
if (!workspace?.folder) {
return undefined;
}
/** The names of files, to show them to the user. */
function namesOf(ids: string[]): string[] {
const files = get(engine.files);
const names = Object.entries(workspace.files)
// the files in the folder, and the new ones, which are saved in it
.filter(
([, file]) =>
file.dirty &&
(file.path === undefined || subfolder(workspace.folder, file.path) !== undefined)
)
.map(([id]) => {
const file = files.get(id);
return file ? get(file).structure.name : id;
});
return names.length > 0 ? askUnsaved(names) : undefined;
return ids.map((id) => {
const file = files.get(id);
return file ? get(file).structure.name : id;
});
}
/** The ids among `ids` of the files that have changes that are not saved. */
function unsavedAmong(ids: string[]): string[] {
const workspace = get(engine.workspace);
return ids.filter((id) => workspace?.files[id]?.dirty);
}
/**
* Opens a folder, which is watched (the desktop app). The folder that was open is closed: the user
* is asked what to do with its unsaved files.
* What to do with the files among `ids` that have unsaved changes, which the user is asked when
* there are some: `undefined` when there are none, `'cancel'` when they do not want to go on.
*/
async function askAboutUnsavedFiles(ids: string[]): Promise<UnsavedChoice | undefined | 'cancel'> {
const unsaved = unsavedAmong(ids);
return unsaved.length > 0 ? askUnsaved(namesOf(unsaved)) : undefined;
}
/** All the open files. */
function allFileIds(): string[] {
return get(engine.order);
}
/**
* Opens a folder, which is watched (the desktop app). Its files are not opened, they are in its
* tree. The folder that was open is closed with its files: the user is asked what to do with the
* unsaved ones.
*/
export async function openFolder() {
const unsaved = await askAboutUnsavedFiles();
if (unsaved === 'cancel') {
return;
}
const ids = await engine.openFolder(unsaved);
if (ids.length > 0) {
boundsManager.fitBoundsOnLoad(ids);
const unsaved = await askAboutUnsavedFiles(allFileIds());
if (unsaved !== 'cancel') {
await engine.openFolder(unsaved);
}
}
/** Closes the open folder and its files: the user is asked what to do with the unsaved ones. */
/** Closes the open folder and all the files: the user is asked what to do with the unsaved ones. */
export async function closeFolder() {
const unsaved = await askAboutUnsavedFiles();
const unsaved = await askAboutUnsavedFiles(allFileIds());
if (unsaved !== 'cancel') {
await engine.closeFolder(unsaved);
}
}
/** Closes files: the user is asked what to do with the ones that have unsaved changes. */
export async function closeFiles(ids: string[]) {
if (ids.length === 0) {
return;
}
const unsaved = await askAboutUnsavedFiles(ids);
if (unsaved !== 'cancel') {
await engine.closeFiles(ids, unsaved);
}
}
/** Closes all the files. */
export function closeAll() {
return closeFiles(allFileIds());
}
/**
* Writes files to the file system. The user is asked first about the ones whose file was also
* changed on the disk, which saving overwrites: they can take the disk instead of their changes.
*/
async function saveFiles(ids: string[]) {
if (ids.length === 0) {
return;
}
const workspace = get(engine.workspace);
const replaced = ids.filter((id) => workspace?.files[id]?.replaced);
if (replaced.length > 0) {
const choice = await askOverwrite(namesOf(replaced));
if (choice === 'cancel') {
return;
}
if (choice === 'reload') {
// they are the files on the disk now, there is nothing to save
await engine.reload(replaced);
ids = ids.filter((id) => !replaced.includes(id));
}
}
if (ids.length > 0) {
await engine.save(ids);
}
}
/** Saves the selected files to the file system, all the ones that have unsaved edits if none is. */
export function saveSelected(selectedFileIds: string[]) {
const workspace = get(engine.workspace);
const ids =
selectedFileIds.length > 0
? selectedFileIds
: Object.keys(workspace?.files ?? {}).filter((id) => workspace?.files[id].dirty);
if (ids.length > 0) {
engine.save(ids);
}
return saveFiles(selectedFileIds.length > 0 ? selectedFileIds : unsavedAmong(allFileIds()));
}
/** Saves all the files that have unsaved edits to the file system. */
export function saveAll() {
const workspace = get(engine.workspace);
const ids = Object.keys(workspace?.files ?? {}).filter((id) => workspace?.files[id].dirty);
return saveFiles(unsavedAmong(allFileIds()));
}
/** Saves a file where the user says, which is where it is from then on (not for several files). */
export async function saveAs(selectedFileIds: string[]) {
if (selectedFileIds.length === 1) {
await engine.saveAs(selectedFileIds[0]);
}
}
/** Opens a file of the tree of the open folder. */
export async function openPath(path: string) {
const ids = await engine.openPaths([path]);
if (ids.length > 0) {
engine.save(ids);
boundsManager.fitBoundsOnLoad(ids);
}
}
+108 -22
View File
@@ -40,6 +40,8 @@
FolderOpen,
FileStack,
FileX,
Save,
SaveAll,
BookOpenText,
ChartArea,
Maximize,
@@ -59,10 +61,22 @@
import { languages } from '$lib/i18n/languages';
import { getURLForLanguage } from '$lib/utils';
import { settings } from '$lib/state/settings';
import { createFile, loadFiles, triggerFileInput } from '$lib/actions/file-actions';
import {
closeAll,
closeFiles,
closeFolder,
createFile,
loadFiles,
openFiles,
openFolder,
saveAll,
saveAs,
saveSelected,
} from '$lib/actions/file-actions';
import UnsavedDialog from '$lib/components/file-list/UnsavedDialog.svelte';
import { engine } from '$lib/engine';
import { allHidden } from '$lib/engine/all-hidden';
import { selectionSize } from '$lib/engine/selection-helpers';
import { selectedFileIds, selectionSize } from '$lib/engine/selection-helpers';
import { boundsManager } from '$lib/state/bounds';
import { onMount } from 'svelte';
@@ -82,10 +96,14 @@
routing,
} = settings;
const { files, selection, canUndo, canRedo, canPaste } = engine;
const { files, selection, canUndo, canRedo, canPaste, workspace } = engine;
let selectionCount = $derived(selectionSize($selection));
let noFiles = $derived($files.size === 0);
// the desktop app: the files are linked to the file system
let desktop = $derived($workspace !== undefined);
let selectedIds = $derived(selectedFileIds($selection));
let anyUnsaved = $derived(Object.values($workspace?.files ?? {}).some((file) => file.dirty));
// metadata and style can be edited for files and tracks only
let filesOrTracksSelected = $derived($selection.type === 'file' || $selection.type === 'track');
@@ -142,11 +160,18 @@
<Shortcut key="+" ctrl={true} />
</Menubar.Item>
<Menubar.Separator />
<Menubar.Item onclick={triggerFileInput}>
<Menubar.Item onclick={openFiles}>
<FolderOpen size="16" />
{i18n._('menu.open')}
<Shortcut key="O" ctrl={true} />
</Menubar.Item>
{#if desktop}
<Menubar.Item onclick={openFolder}>
<FolderOpen size="16" />
{i18n._('menu.open_folder')}
<Shortcut key="O" ctrl={true} shift={true} />
</Menubar.Item>
{/if}
<Menubar.Separator />
<Menubar.Item onclick={() => engine.duplicate()} disabled={selectionCount == 0}>
<Copy size="16" />
@@ -154,19 +179,60 @@
<Shortcut key="D" ctrl={true} />
</Menubar.Item>
<Menubar.Separator />
<Menubar.Item
onclick={() => engine.delete(true)}
disabled={selectionCount == 0}
>
<FileX size="16" />
{i18n._('menu.delete')}
<Shortcut key="⌫" ctrl={true} />
</Menubar.Item>
<Menubar.Item onclick={() => engine.deleteAll()} disabled={noFiles}>
<FileX size="16" />
{i18n._('menu.delete_all')}
<Shortcut key="⌫" ctrl={true} shift={true} />
</Menubar.Item>
{#if desktop}
<Menubar.Item
onclick={() => saveSelected(selectedIds)}
disabled={selectionCount == 0 && !anyUnsaved}
>
<Save size="16" />
{i18n._('menu.save')}
<Shortcut key="S" ctrl={true} />
</Menubar.Item>
<Menubar.Item onclick={saveAll} disabled={!anyUnsaved}>
<Save size="16" />
{i18n._('menu.save_all')}
<Shortcut key="S" ctrl={true} shift={true} />
</Menubar.Item>
<Menubar.Item
onclick={() => saveAs(selectedIds)}
disabled={selectedIds.length !== 1}
>
<SaveAll size="16" />
{i18n._('menu.save_as')}
</Menubar.Item>
<Menubar.Separator />
<Menubar.Item
onclick={() => closeFiles(selectedIds)}
disabled={selectedIds.length === 0}
>
<FileX size="16" />
{i18n._('menu.close')}
<Shortcut key="W" ctrl={true} />
</Menubar.Item>
<Menubar.Item onclick={closeAll} disabled={noFiles}>
<FileX size="16" />
{i18n._('menu.close_all')}
<Shortcut key="W" ctrl={true} shift={true} />
</Menubar.Item>
<Menubar.Item onclick={closeFolder} disabled={!$workspace?.folder}>
<FileX size="16" />
{i18n._('menu.close_folder')}
</Menubar.Item>
{:else}
<Menubar.Item
onclick={() => engine.delete(true)}
disabled={selectionCount == 0}
>
<FileX size="16" />
{i18n._('menu.delete')}
<Shortcut key="⌫" ctrl={true} />
</Menubar.Item>
<Menubar.Item onclick={() => engine.deleteAll()} disabled={noFiles}>
<FileX size="16" />
{i18n._('menu.delete_all')}
<Shortcut key="⌫" ctrl={true} shift={true} />
</Menubar.Item>
{/if}
<Menubar.Separator />
<Menubar.Item
onclick={() => (exportState.current = ExportState.SELECTION)}
@@ -174,7 +240,7 @@
>
<Download size="16" />
{i18n._('menu.export')}
<Shortcut key="S" ctrl={true} />
{#if !desktop}<Shortcut key="S" ctrl={true} />{/if}
</Menubar.Item>
<Menubar.Item
onclick={() => (exportState.current = ExportState.ALL)}
@@ -182,7 +248,7 @@
>
<Download size="16" />
{i18n._('menu.export_all')}
<Shortcut key="S" ctrl={true} shift={true} />
{#if !desktop}<Shortcut key="S" ctrl={true} shift={true} />{/if}
</Menubar.Item>
</Menubar.Content>
</Menubar.Menu>
@@ -498,6 +564,7 @@
</div>
<Export />
<UnsavedDialog />
<LayerControlSettings bind:open={layerSettingsOpen} />
<svelte:window
@@ -519,8 +586,26 @@
if (e.key === '+' && (e.metaKey || e.ctrlKey)) {
createFile();
e.preventDefault();
} else if (e.key === 'o' && (e.metaKey || e.ctrlKey)) {
triggerFileInput();
} else if ((e.key === 'o' || e.key === 'O') && (e.metaKey || e.ctrlKey)) {
if (desktop && e.shiftKey) {
openFolder();
} else {
openFiles();
}
e.preventDefault();
} else if (desktop && (e.key === 'w' || e.key === 'W') && (e.metaKey || e.ctrlKey)) {
if (e.shiftKey) {
closeAll();
} else {
closeFiles(selectedIds);
}
e.preventDefault();
} else if (desktop && (e.key === 's' || e.key === 'S') && (e.metaKey || e.ctrlKey)) {
if (e.shiftKey) {
saveAll();
} else {
saveSelected(selectedIds);
}
e.preventDefault();
} else if (e.key === 'd' && (e.metaKey || e.ctrlKey)) {
engine.duplicate();
@@ -557,7 +642,8 @@
}
e.preventDefault();
} else if ((e.key === 'Backspace' || e.key === 'Delete') && (e.metaKey || e.ctrlKey)) {
if (!targetInput) {
// the desktop app closes files instead
if (!targetInput && !desktop) {
if (e.shiftKey) {
engine.deleteAll();
} else {
@@ -31,6 +31,11 @@ export function allFileIds(): string[] {
}
async function exportFiles(fileIds: string[], options: ExportOptions) {
if (get(engine.workspace)) {
// the desktop app: the user chooses where the files go
await engine.exportFiles(fileIds, options);
return;
}
const files = (
await Promise.all(
fileIds.map(async (fileId) => {
@@ -0,0 +1,69 @@
<script lang="ts">
import * as AlertDialog from '$lib/components/ui/alert-dialog';
import { Button } from '$lib/components/ui/button';
import { i18n } from '$lib/i18n/i18n.svelte';
import { unsavedPrompt } from './unsaved-prompt.svelte';
let prompt = $derived(unsavedPrompt.current);
/** Closes the dialog, and gives the answer to whoever asked. */
function answer(choice: 'save' | 'discard' | 'reload' | 'cancel') {
const current = unsavedPrompt.current;
unsavedPrompt.current = undefined;
if (current?.kind === 'unsaved' && choice !== 'reload') {
current.resolve(choice);
} else if (current?.kind === 'overwrite') {
current.resolve(
choice === 'save' ? 'overwrite' : choice === 'reload' ? 'reload' : 'cancel'
);
}
}
</script>
<AlertDialog.Root
open={prompt !== undefined}
onOpenChange={(open) => {
if (!open) {
answer('cancel');
}
}}
>
<AlertDialog.Content>
{#if prompt}
<AlertDialog.Header>
<AlertDialog.Title>
{prompt.kind === 'unsaved'
? i18n._('menu.unsaved_title')
: i18n._('menu.overwrite_title')}
</AlertDialog.Title>
<AlertDialog.Description>
{prompt.kind === 'unsaved'
? i18n._('menu.unsaved_description')
: i18n._('menu.overwrite_description')}
</AlertDialog.Description>
</AlertDialog.Header>
<ul class="list-disc pl-6 text-sm max-h-40 overflow-y-auto">
{#each prompt.names as name}
<li>{name}</li>
{/each}
</ul>
<AlertDialog.Footer>
<Button variant="outline" onclick={() => answer('cancel')}>
{i18n._('menu.cancel')}
</Button>
{#if prompt.kind === 'unsaved'}
<Button variant="destructive" onclick={() => answer('discard')}>
{i18n._('menu.discard')}
</Button>
{:else}
<Button variant="destructive" onclick={() => answer('reload')}>
{i18n._('menu.reload')}
</Button>
{/if}
<Button onclick={() => answer('save')}>
{prompt.kind === 'unsaved' ? i18n._('menu.save') : i18n._('menu.overwrite')}
</Button>
</AlertDialog.Footer>
{/if}
</AlertDialog.Content>
</AlertDialog.Root>
@@ -0,0 +1,32 @@
import type { UnsavedChoice } from '$lib/engine';
type Prompt =
| { kind: 'unsaved'; names: string[]; resolve: (choice: UnsavedChoice | 'cancel') => void }
| {
kind: 'overwrite';
names: string[];
resolve: (choice: 'overwrite' | 'reload' | 'cancel') => void;
};
/** The question that the user is asked, shown by `UnsavedDialog`. */
export const unsavedPrompt: { current: Prompt | undefined } = $state({ current: undefined });
/**
* Asks what to do with files that have unsaved changes when they are closed: save them, discard
* the changes, or `'cancel'` to keep going as before.
*/
export function askUnsaved(names: string[]): Promise<UnsavedChoice | 'cancel'> {
return new Promise((resolve) => {
unsavedPrompt.current = { kind: 'unsaved', names, resolve };
});
}
/**
* Asks what to do with files that have changes and were also changed on the disk: `'overwrite'` the
* disk, `'reload'` the disk (the changes are lost), or `'cancel'`.
*/
export function askOverwrite(names: string[]): Promise<'overwrite' | 'reload' | 'cancel'> {
return new Promise((resolve) => {
unsavedPrompt.current = { kind: 'overwrite', names, resolve };
});
}
+9 -2
View File
@@ -1,7 +1,7 @@
import { invoke } from '@tauri-apps/api/core';
import { listen } from '@tauri-apps/api/event';
import type { Outcome, RouteAttributes, StorageOpened } from 'gpx-wasm';
import type { Backend, WorkspaceState } from './types';
import type { Backend, FolderEntry, WorkspaceState } from './types';
/** An array that a command returns as its bytes. */
const array = async <T>(
@@ -211,9 +211,16 @@ export async function createTauriBackend(): Promise<Backend> {
desktop: {
workspace: () => invoke<WorkspaceState>('workspace_state'),
openFiles: () => invoke<Outcome>('open_files'),
openPaths: (paths) => invoke<Outcome>('open_paths', { paths }),
openFolder: (unsaved) => invoke<Outcome>('open_folder', { unsaved }),
closeFolder: (unsaved) => invoke<Outcome>('close_folder', { unsaved }),
saveFiles: (fileIds) => invoke('save_files', { fileIds }),
closeFiles: (fileIds, unsaved) => invoke<Outcome>('close_files', { fileIds, unsaved }),
saveFiles: (fileIds) => invoke<boolean>('save_files', { fileIds }),
saveFileAs: (fileId) => invoke<boolean>('save_file_as', { fileId }),
reloadFiles: (fileIds) => invoke<Outcome>('reload_files', { fileIds }),
exportFiles: (fileIds, options) =>
invoke<boolean>('export_files', { fileIds, options }),
listFolder: (path) => invoke<FolderEntry[]>('list_folder', { path }),
listen: async (onEngineChanged, onWorkspaceChanged, onFilesOpened) => {
await listen<Outcome>('files-opened', (event) => onFilesOpened(event.payload));
await listen<Outcome>('engine-changed', (event) => onEngineChanged(event.payload));
+48 -14
View File
@@ -10,16 +10,33 @@ import type {
TrackpointDetails,
WaypointDetails,
} from 'gpx-wasm';
import type { MergeType, SelectMode, SplitType } from '../types';
import type { ExportOptions, MergeType, SelectMode, SplitType } from '../types';
/** What the desktop app knows of a file of the engine and the file system. */
export type FileStatus = {
/** Where the file is on the disk, absent for a file that only is in the engine. */
path?: string;
/** The file has edits that the file on the disk does not have (or is not on the disk). */
/**
* The folder where the file is written the first time it is saved, for a file that is not on
* the disk and comes from (or was made in) the open folder.
*/
folder?: string;
/**
* The file is not the one on the disk: it has edits, is not on the disk, or the file on the
* disk is not the one that was read (it was replaced, or is gone).
*/
dirty: boolean;
/** The file on the disk was changed by something else while this one had edits. */
conflict: boolean;
/** The file on the disk was replaced by something else: saving overwrites it. */
replaced: boolean;
/** The file on the disk is gone. */
missing: boolean;
};
/** An entry of the tree of the open folder: a folder, or a GPX file. */
export type FolderEntry = {
name: string;
path: string;
isDir: boolean;
};
/** What to do with the files of a folder that have unsaved edits when it is closed. */
@@ -39,22 +56,39 @@ export interface DesktopFiles {
workspace(): Promise<WorkspaceState>;
/** Opens the files that the user picks, which get selected. */
openFiles(): Promise<Outcome>;
/** Opens files of the file system by their path, like the ones of the tree of the folder. */
openPaths(paths: string[]): Promise<Outcome>;
/**
* Opens the folder that the user picks: its files are opened, and it is watched. The folder
* that was open is closed (see `closeFolder` for `unsaved`).
* Opens the folder that the user picks, which is watched. Its files are not opened: they are
* in its tree (`listFolder`). The folder that was open is closed with its files (see
* `closeFiles` for `unsaved`).
*/
openFolder(unsaved?: UnsavedChoice): Promise<Outcome>;
/**
* Closes the open folder: it is not watched anymore, and its files are closed. What to do with
* the ones that have unsaved edits: `save` them first, `discard` the edits, or keep them open
* when it is not said. Rejects, and closes nothing, if they could not be saved.
*/
/** Closes the open folder: it is not watched anymore, and its files are closed. */
closeFolder(unsaved?: UnsavedChoice): Promise<Outcome>;
/**
* Writes files to the disk, replacing what is there (the user is asked where to put the ones
* that are not on the disk yet).
* Closes files. What to do with the ones that have unsaved edits: `save` them first (the user
* is asked where to put the ones that have no place), `discard` the edits, or keep them open
* when it is not said.
*/
saveFiles(fileIds: string[]): Promise<void>;
closeFiles(fileIds: string[], unsaved?: UnsavedChoice): Promise<Outcome>;
/**
* Writes files to the disk, replacing what is there (the user is asked where to put the ones
* that have no place: a file, or a folder for several of them). Resolves to false if the user
* cancelled.
*/
saveFiles(fileIds: string[]): Promise<boolean>;
/** Writes a file where the user says, which is where it is from then on. False if cancelled. */
saveFileAs(fileId: string): Promise<boolean>;
/** Reads files again from the disk: their edits are lost (one undo step). */
reloadFiles(fileIds: string[]): Promise<Outcome>;
/**
* Writes copies of files where the user says (a file, or a folder for several). The files stay
* as they are. False if the user cancelled.
*/
exportFiles(fileIds: string[], options: ExportOptions): Promise<boolean>;
/** The folders and GPX files in a folder of the open folder (the open folder by default). */
listFolder(path?: string): Promise<FolderEntry[]>;
/**
* Tells when the engine changed without being asked, because the files on the disk did (the
* outcome to apply), when the status of the files changed, and when files were opened with the
+67 -18
View File
@@ -11,6 +11,7 @@ import { categoryIntervals } from '$lib/engine/trackpoint-categories';
import { selectedElementIds, type FileTreeNode } from '$lib/engine/selection-helpers';
import type { RouteAttributes } from 'gpx-wasm';
import { loadBackend, type Backend } from './backend';
import type { DesktopFiles } from './backend/types';
import { readFileState } from './file-state';
import { StatisticsReader } from './statistics';
import { openStoredFiles } from './storage';
@@ -29,6 +30,7 @@ import {
type SelectMode,
type SplitType,
type StatisticsRequest,
type FolderEntry,
type UnsavedChoice,
type WorkspaceState,
type TrackpointDetails,
@@ -554,41 +556,88 @@ class Engine {
return this.addedBy((b) => b.desktop!.openFiles());
}
/**
* Opens the folder that the user picks (instead of the one that was open, which is closed like
* `closeFolder` does, `unsaved` being what to do with its unsaved files): its files are opened,
* and it is watched: the files that appear, change or disappear in it are brought in the engine.
* New files are saved in it. Resolves to the ids of the files that were added.
*/
async openFolder(unsaved?: UnsavedChoice): Promise<string[]> {
return this.addedBy((b) => b.desktop!.openFolder(unsaved));
/** Opens files of the file system by their path, which get selected. Resolves to the new ids. */
async openPaths(paths: string[]): Promise<string[]> {
return this.addedBy((b) => b.desktop!.openPaths(paths));
}
/**
* Closes the open folder: it is not watched anymore, and its files are closed. `unsaved` says
* what to do with the ones that have unsaved edits: they are saved first, or their edits are
* discarded (they stay open when it is not said).
* Opens the folder that the user picks (instead of the one that was open, which is closed with
* its files like `closeFiles` does, `unsaved` being what to do with the unsaved ones). The
* folder is watched, and its tree is read with `listFolder`: its files are not opened.
*/
async openFolder(unsaved?: UnsavedChoice) {
await this.run((b) => b.desktop!.openFolder(unsaved));
await this.enqueue(async () => this.refreshWorkspace(this.backend!));
}
/** Closes the open folder and all the files: see `closeFiles` for `unsaved`. */
async closeFolder(unsaved?: UnsavedChoice) {
await this.run((b) => b.desktop!.closeFolder(unsaved));
await this.enqueue(async () => this.refreshWorkspace(this.backend!));
}
/**
* Writes files to the file system, replacing what is there; they are not edited anymore from
* then on. The user is asked where to put the ones that are not on the file system yet.
* Closes files. `unsaved` says what to do with the ones that have unsaved edits: they are saved
* first, or their edits are lost (they stay open when it is not said).
*/
async save(fileIds: string[]) {
async closeFiles(fileIds: string[], unsaved?: UnsavedChoice) {
await this.run((b) => b.desktop!.closeFiles(fileIds, unsaved));
await this.enqueue(async () => this.refreshWorkspace(this.backend!));
}
/**
* Writes files to the file system, replacing what is there; they are not edited anymore from
* then on. The user is asked where to put the ones that have no place yet. Resolves to false
* when the user cancelled or the files could not be saved.
*/
async save(fileIds: string[]): Promise<boolean> {
return this.desktopCall((desktop) => desktop.saveFiles(fileIds));
}
/** Writes a file where the user says, which is where it is from then on. */
async saveAs(fileId: string): Promise<boolean> {
return this.desktopCall((desktop) => desktop.saveFileAs(fileId));
}
/** Writes copies of files where the user says. The files stay as they are. */
async exportFiles(fileIds: string[], options: ExportOptions): Promise<boolean> {
return this.desktopCall((desktop) => desktop.exportFiles(fileIds, options));
}
/** Reads files again from the file system: their edits are lost (one undo step). */
async reload(fileIds: string[]) {
await this.run((b) => b.desktop!.reloadFiles(fileIds));
await this.enqueue(async () => this.refreshWorkspace(this.backend!));
}
/** The folders and GPX files in a folder of the open folder (the open folder by default). */
async listFolder(path?: string): Promise<FolderEntry[]> {
await this.ready;
await this.enqueue(async () => {
const backend = this.backend!;
return this.enqueue(async () => {
try {
await backend.desktop!.saveFiles(fileIds);
return await this.backend!.desktop!.listFolder(path);
} catch (error) {
console.error('The files could not be saved', error);
console.error('The folder could not be read', error);
return [];
}
});
}
/** Calls the file system of the desktop app, and keeps the status of the files up to date. */
private async desktopCall(call: (desktop: DesktopFiles) => Promise<boolean>): Promise<boolean> {
await this.ready;
return this.enqueue(async () => {
const backend = this.backend!;
let done = false;
try {
done = await call(backend.desktop!);
} catch (error) {
console.error('The files could not be written', error);
this._lastError.set(String(error));
}
await this.refreshWorkspace(backend);
return done;
});
}
+1 -1
View File
@@ -4,7 +4,7 @@ import type { Visibility } from '$lib/engine/file-visibility';
import type { CategoryIntervals } from '$lib/engine/trackpoint-categories';
import type { FileStructure, GlobalStatistics } from 'gpx-wasm';
export type { FileStatus, UnsavedChoice, WorkspaceState } from './backend/types';
export type { FileStatus, FolderEntry, UnsavedChoice, WorkspaceState } from './backend/types';
export type {
Clipboard,