mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2026-10-19 09:22:12 +00:00
cleaning
This commit is contained in:
1 parent
8fd47d8b18
commit
5373d0f0d4
15 files changed
+339
-408
No files matched your search
@@ -5,8 +5,8 @@
|
||||
import { ScrollArea } from '$lib/components/ui/scroll-area/index';
|
||||
import ExplorerTree from './ExplorerTree.svelte';
|
||||
import ExplorerRow from './ExplorerRow.svelte';
|
||||
import { openFiles } from './actions';
|
||||
import { clickedInExplorer, explorerSelection, renaming } from './state.svelte';
|
||||
import { clickedInExplorer, explorerSelection, renaming, treeRows } from './state.svelte';
|
||||
import { openPaths } from '$lib/actions/file-actions';
|
||||
import { i18n } from '$lib/i18n/i18n.svelte';
|
||||
import { engine } from '$lib/engine';
|
||||
import { selectedFileIds } from '$lib/engine/selection-helpers';
|
||||
@@ -52,22 +52,6 @@
|
||||
}
|
||||
});
|
||||
|
||||
/** Enter opens the selected files for good. */
|
||||
function onkeydown(event: KeyboardEvent) {
|
||||
if (renaming.path !== undefined || !container || event.key !== 'Enter') return;
|
||||
const kinds = new Map(
|
||||
[...container.querySelectorAll<HTMLElement>('[data-explorer-path]')].map((row) => [
|
||||
row.dataset.explorerPath!,
|
||||
row.dataset.kind,
|
||||
])
|
||||
);
|
||||
const files = explorerSelection.paths.filter((path) => kinds.get(path) === 'file');
|
||||
if (files.length > 0) {
|
||||
openFiles(files, false);
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the folders that hold a file and scrolls to the file, if it is not in view already. Its
|
||||
* row may not be there yet, because the content of a folder is read when it is opened.
|
||||
@@ -95,6 +79,18 @@
|
||||
frame = requestAnimationFrame(scroll);
|
||||
return () => cancelAnimationFrame(frame);
|
||||
}
|
||||
|
||||
/** Enter opens the selected files for good. */
|
||||
function onkeydown(event: KeyboardEvent) {
|
||||
if (renaming.path !== undefined || event.key !== 'Enter') return;
|
||||
const files = treeRows(container)
|
||||
.filter((row) => row.kind === 'file' && explorerSelection.paths.includes(row.path))
|
||||
.map((row) => row.path);
|
||||
if (files.length > 0) {
|
||||
openPaths(files);
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!--
|
||||
|
||||
@@ -12,19 +12,17 @@
|
||||
import type { Snippet } from 'svelte';
|
||||
import { i18n } from '$lib/i18n/i18n.svelte';
|
||||
import { engine } from '$lib/engine';
|
||||
import { fileIdAt } from '$lib/engine/workspace';
|
||||
import { closeFiles, closeFolder } from '$lib/actions/file-actions';
|
||||
import { editableName, fileIdAt, isWithin, parentOf } from '$lib/engine/workspace';
|
||||
import { closeFiles, closeFolder, openPaths, showEngineError } from '$lib/actions/file-actions';
|
||||
import { isMac } from '$lib/utils';
|
||||
import { openFiles, showError } from './actions';
|
||||
import NameInput from './NameInput.svelte';
|
||||
import {
|
||||
clickedInExplorer,
|
||||
creating,
|
||||
dragging,
|
||||
editableName,
|
||||
explorerSelection,
|
||||
isWithin,
|
||||
parentOf,
|
||||
renaming,
|
||||
treeRows,
|
||||
} from './state.svelte';
|
||||
|
||||
let {
|
||||
@@ -58,17 +56,6 @@
|
||||
let editing = $derived(renaming.path === path);
|
||||
let target = $derived(dragging.target === path);
|
||||
|
||||
/** The rows of the tree, as they are shown. */
|
||||
function rows(): HTMLElement[] {
|
||||
const tree = row?.closest('[data-explorer]');
|
||||
return tree ? [...tree.querySelectorAll<HTMLElement>('[data-explorer-path]')] : [];
|
||||
}
|
||||
|
||||
/** Whether a path of the tree is a GPX file, a folder, or the open folder. */
|
||||
function kindOf(other: string): string | undefined {
|
||||
return rows().find((r) => r.dataset.explorerPath === other)?.dataset.kind;
|
||||
}
|
||||
|
||||
/** What the menu and dragging act on: the selection, if the row is in it. */
|
||||
function targets(): string[] {
|
||||
return selected ? explorerSelection.paths : [path];
|
||||
@@ -94,9 +81,9 @@
|
||||
event.stopPropagation();
|
||||
} else if (event.shiftKey && explorerSelection.anchor !== undefined) {
|
||||
// everything shown between the row where the selection began and this one
|
||||
const paths = rows()
|
||||
.filter((r) => r.dataset.kind !== 'root')
|
||||
.map((r) => r.dataset.explorerPath!);
|
||||
const paths = treeRows(row)
|
||||
.filter((other) => other.kind !== 'root')
|
||||
.map((other) => other.path);
|
||||
const [from, to] = [paths.indexOf(explorerSelection.anchor), paths.indexOf(path)];
|
||||
if (from >= 0 && to >= 0) {
|
||||
explorerSelection.paths = paths.slice(Math.min(from, to), Math.max(from, to) + 1);
|
||||
@@ -111,7 +98,7 @@
|
||||
if (fileId !== undefined) {
|
||||
engine.select([fileId]);
|
||||
} else {
|
||||
openFiles([path], true);
|
||||
openPaths([path], true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -120,7 +107,7 @@
|
||||
function ondblclick() {
|
||||
if (kind === 'file' && !editing) {
|
||||
clickedInExplorer.path = path;
|
||||
openFiles([path], false);
|
||||
openPaths([path], false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,9 +125,11 @@
|
||||
}
|
||||
|
||||
function openSelected() {
|
||||
const files = targets().filter((other) => kindOf(other) === 'file');
|
||||
const files = treeRows(row)
|
||||
.filter((other) => other.kind === 'file' && targets().includes(other.path))
|
||||
.map((other) => other.path);
|
||||
if (files.length > 0) {
|
||||
openFiles(files, false);
|
||||
openPaths(files);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,7 +144,7 @@
|
||||
const paths = targets();
|
||||
explorerSelection.paths = [];
|
||||
if (!(await engine.deletePaths(paths))) {
|
||||
showError();
|
||||
showEngineError();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,26 +156,10 @@
|
||||
const name_ = value.trim();
|
||||
if (name_ === '' || name_ === editableName(name, kind !== 'file')) return;
|
||||
if (!(await engine.renamePath(path, name_))) {
|
||||
showError();
|
||||
showEngineError();
|
||||
}
|
||||
}
|
||||
|
||||
function onkeydown(event: KeyboardEvent) {
|
||||
// not for the folder row it is in, which would open or close
|
||||
event.stopPropagation();
|
||||
const input = event.currentTarget as HTMLInputElement;
|
||||
if (event.key === 'Enter') {
|
||||
commit(input.value);
|
||||
} else if (event.key === 'Escape') {
|
||||
renaming.path = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function focus(input: HTMLInputElement) {
|
||||
input.focus();
|
||||
input.select();
|
||||
}
|
||||
|
||||
// dragging into folders
|
||||
|
||||
function ondragstart(event: DragEvent) {
|
||||
@@ -235,7 +208,7 @@
|
||||
const paths = dragging.paths;
|
||||
ondragend();
|
||||
if (paths.length > 0 && !(await engine.movePaths(paths, dropFolder))) {
|
||||
showError();
|
||||
showEngineError();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -273,14 +246,10 @@
|
||||
>
|
||||
{@render icon?.()}
|
||||
{#if editing}
|
||||
<input
|
||||
class="h-6 grow min-w-0 px-1 text-sm font-normal not-italic bg-background border rounded outline-none"
|
||||
<NameInput
|
||||
value={editableName(name, kind !== 'file')}
|
||||
{@attach focus}
|
||||
{onkeydown}
|
||||
onkeyup={(event) => event.stopPropagation()}
|
||||
onclick={(event) => event.stopPropagation()}
|
||||
onblur={(event) => commit(event.currentTarget.value)}
|
||||
oncommit={commit}
|
||||
oncancel={() => (renaming.path = undefined)}
|
||||
/>
|
||||
{:else}
|
||||
<span class="grow truncate text-left">{name}</span>
|
||||
|
||||
@@ -8,11 +8,12 @@
|
||||
import ExplorerTree from './ExplorerTree.svelte';
|
||||
import ExplorerRow from './ExplorerRow.svelte';
|
||||
import { engine, type FolderEntry } from '$lib/engine';
|
||||
import { fileIdAt } from '$lib/engine/workspace';
|
||||
import { editableName, fileIdAt } from '$lib/engine/workspace';
|
||||
import { i18n } from '$lib/i18n/i18n.svelte';
|
||||
import { currentTool, Tool } from '$lib/components/toolbar/tools';
|
||||
import { showError } from './actions';
|
||||
import { creating, editableName, explorerSelection } from './state.svelte';
|
||||
import { showEngineError } from '$lib/actions/file-actions';
|
||||
import NameInput from './NameInput.svelte';
|
||||
import { creating, explorerSelection } from './state.svelte';
|
||||
|
||||
let {
|
||||
path,
|
||||
@@ -69,24 +70,13 @@
|
||||
created = await engine.createFolder(parent, name);
|
||||
}
|
||||
if (created === undefined) {
|
||||
showError();
|
||||
showEngineError();
|
||||
return;
|
||||
}
|
||||
explorerSelection.paths = [created];
|
||||
explorerSelection.anchor = created;
|
||||
}
|
||||
|
||||
function onkeydown(event: KeyboardEvent) {
|
||||
// not for the explorer, which opens the selected files with Enter
|
||||
event.stopPropagation();
|
||||
const input = event.currentTarget as HTMLInputElement;
|
||||
if (event.key === 'Enter') {
|
||||
create(input.value, true);
|
||||
} else if (event.key === 'Escape') {
|
||||
creating.parent = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
// read again when something changed on the file system
|
||||
$folderVersion;
|
||||
@@ -110,14 +100,12 @@
|
||||
{:else}
|
||||
<Folder size="16" class="shrink-0" />
|
||||
{/if}
|
||||
<input
|
||||
class="h-6 grow min-w-0 px-1 text-sm bg-background border rounded outline-none"
|
||||
<NameInput
|
||||
placeholder={i18n._(
|
||||
kind === 'file' ? 'menu.explorer_menu.new_file' : 'menu.explorer_menu.new_folder'
|
||||
)}
|
||||
{@attach (input) => input.focus()}
|
||||
{onkeydown}
|
||||
onblur={(event) => create(event.currentTarget.value, false)}
|
||||
oncommit={create}
|
||||
oncancel={() => (creating.parent = undefined)}
|
||||
/>
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
<script lang="ts">
|
||||
let {
|
||||
value = '',
|
||||
placeholder,
|
||||
oncommit,
|
||||
oncancel,
|
||||
}: {
|
||||
value?: string;
|
||||
placeholder?: string;
|
||||
/** The name is given: with Enter (`enter`), or by leaving the field. */
|
||||
oncommit: (value: string, enter: boolean) => void;
|
||||
/** Escape: no name is given. */
|
||||
oncancel: () => void;
|
||||
} = $props();
|
||||
|
||||
function onkeydown(event: KeyboardEvent) {
|
||||
// not for the row or the explorer it is in (a folder opens with Enter or Space, the selected
|
||||
// files open with Enter)
|
||||
event.stopPropagation();
|
||||
if (event.key === 'Enter') {
|
||||
oncommit((event.currentTarget as HTMLInputElement).value, true);
|
||||
} else if (event.key === 'Escape') {
|
||||
oncancel();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- the name of a file or a folder of the explorer, being given or changed -->
|
||||
<input
|
||||
class="h-6 grow min-w-0 px-1 text-sm font-normal not-italic bg-background border rounded outline-none"
|
||||
{value}
|
||||
{placeholder}
|
||||
{@attach (input) => {
|
||||
input.focus();
|
||||
input.select();
|
||||
}}
|
||||
{onkeydown}
|
||||
onkeyup={(event) => event.stopPropagation()}
|
||||
onclick={(event) => event.stopPropagation()}
|
||||
onblur={(event) => oncommit(event.currentTarget.value, false)}
|
||||
/>
|
||||
@@ -1,24 +0,0 @@
|
||||
import { get } from 'svelte/store';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { engine } from '$lib/engine';
|
||||
import { i18n } from '$lib/i18n/i18n.svelte';
|
||||
import { boundsManager } from '$lib/state/bounds';
|
||||
|
||||
/**
|
||||
* Opens files of the tree: as a preview (a single click), which the next preview replaces, or for
|
||||
* good. The map fits the files that were opened.
|
||||
*/
|
||||
export async function openFiles(paths: string[], preview: boolean) {
|
||||
const ids = await engine.openPaths(paths, preview);
|
||||
if (ids.length > 0) {
|
||||
boundsManager.fitBoundsOnLoad(ids);
|
||||
}
|
||||
}
|
||||
|
||||
/** Tells why what the explorer did failed (a key of the dictionaries, or a message). */
|
||||
export function showError() {
|
||||
const error = get(engine.lastError);
|
||||
if (error !== undefined && error !== 'nothing to do') {
|
||||
toast.error(i18n._(error, error));
|
||||
}
|
||||
}
|
||||
@@ -28,24 +28,11 @@ export const dragging: { paths: string[]; target: string | undefined } = $state(
|
||||
target: undefined,
|
||||
});
|
||||
|
||||
/** `\` and `/` alike. */
|
||||
function isSeparator(char: string | undefined) {
|
||||
return char === '/' || char === '\\';
|
||||
}
|
||||
|
||||
/** The folder that holds `path`. */
|
||||
export function parentOf(path: string): string {
|
||||
const index = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\'));
|
||||
return index > 0 ? path.slice(0, index) : path;
|
||||
}
|
||||
|
||||
/** Whether `path` is `folder` or something in it. */
|
||||
export function isWithin(path: string, folder: string): boolean {
|
||||
return path === folder || (path.startsWith(folder) && isSeparator(path[folder.length]));
|
||||
}
|
||||
|
||||
/** The name of a file without its extension, or the name of a folder. */
|
||||
export function editableName(name: string, isDir: boolean): string {
|
||||
const dot = name.lastIndexOf('.');
|
||||
return isDir || dot <= 0 ? name : name.slice(0, dot);
|
||||
/** The rows of the explorer that `element` is in, as they are shown: their path and kind. */
|
||||
export function treeRows(element: Element | null | undefined): { path: string; kind: string }[] {
|
||||
const tree = element?.closest('[data-explorer]');
|
||||
return [...(tree?.querySelectorAll<HTMLElement>('[data-explorer-path]') ?? [])].map((row) => ({
|
||||
path: row.dataset.explorerPath!,
|
||||
kind: row.dataset.kind!,
|
||||
}));
|
||||
}
|
||||
@@ -9,8 +9,7 @@
|
||||
import { editMetadata } from '$lib/components/file-list/metadata/utils.svelte';
|
||||
import { engine, type FileState } from '$lib/engine';
|
||||
import type { FileTreeNode } from '$lib/engine/selection-helpers';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { get } from 'svelte/store';
|
||||
import { showEngineError } from '$lib/actions/file-actions';
|
||||
|
||||
let {
|
||||
fileState,
|
||||
@@ -58,10 +57,8 @@
|
||||
// the metadata of the selected elements are changed. In the desktop app, a file on
|
||||
// the disk is renamed with it, which is refused if the name is the one of another
|
||||
engine.metadata(name, description).then((changed) => {
|
||||
const error = get(engine.lastError);
|
||||
if (!changed && error !== undefined && error !== 'nothing to do') {
|
||||
// a key of the dictionaries, when it comes from the desktop app
|
||||
toast.error(i18n._(error, error));
|
||||
if (!changed) {
|
||||
showEngineError();
|
||||
}
|
||||
});
|
||||
}}
|
||||
|
||||
Reference in new issue
Block a user