mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2026-10-19 09:22:12 +00:00
file explorer
This commit is contained in:
1 parent
b0a3c3a9ba
commit
ed90ba6a1f
14 files changed
+263
-19
No files matched your search
@@ -10,7 +10,8 @@
|
|||||||
id: string | number;
|
id: string | number;
|
||||||
class?: ClassValue;
|
class?: ClassValue;
|
||||||
trigger: Snippet;
|
trigger: Snippet;
|
||||||
content: Snippet;
|
/** Gets whether the node is open, for contents that are only worth building then. */
|
||||||
|
content: Snippet<[boolean]>;
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
let state = getContext<CollapsibleTreeState>('collapsible-tree-state');
|
let state = getContext<CollapsibleTreeState>('collapsible-tree-state');
|
||||||
@@ -88,6 +89,6 @@
|
|||||||
</Button>
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
<Collapsible.Content>
|
<Collapsible.Content>
|
||||||
{@render props.content()}
|
{@render props.content(open.current)}
|
||||||
</Collapsible.Content>
|
</Collapsible.Content>
|
||||||
</Collapsible.Root>
|
</Collapsible.Root>
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { CollapsibleTree, CollapsibleTreeNode } from '$lib/components/collapsible-tree/index';
|
||||||
|
import { ScrollArea } from '$lib/components/ui/scroll-area/index';
|
||||||
|
import ExplorerTree from './ExplorerTree.svelte';
|
||||||
|
import { i18n } from '$lib/i18n/i18n.svelte';
|
||||||
|
import { engine } from '$lib/engine';
|
||||||
|
import { folderName } from '$lib/engine/workspace';
|
||||||
|
|
||||||
|
const { workspace } = engine;
|
||||||
|
|
||||||
|
let folder = $derived($workspace?.folder);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
The tree of the open folder: its folders and GPX files, one level read at a time. The files are
|
||||||
|
opened from it (the file list has the ones that are open).
|
||||||
|
-->
|
||||||
|
{#if folder}
|
||||||
|
<div class="flex flex-col h-full">
|
||||||
|
<div class="px-2 py-1 text-sm font-semibold shrink-0">{i18n._('menu.explorer')}</div>
|
||||||
|
<ScrollArea class="grow min-h-0">
|
||||||
|
<div class="flex flex-col pl-1 pr-3 pb-1">
|
||||||
|
<!-- starts over when another folder is opened -->
|
||||||
|
{#key folder}
|
||||||
|
<!-- the folder itself, which is open at first -->
|
||||||
|
<CollapsibleTree side="left" defaultState="open">
|
||||||
|
<CollapsibleTreeNode id="folder">
|
||||||
|
{#snippet trigger()}
|
||||||
|
<span class="grow truncate text-left" title={folder}>
|
||||||
|
{folderName(folder)}
|
||||||
|
</span>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet content(opened)}
|
||||||
|
<!-- what is in it is only read when it is open -->
|
||||||
|
{#if opened}
|
||||||
|
<div class="ml-4">
|
||||||
|
<!-- its folders are closed at first -->
|
||||||
|
<CollapsibleTree side="left" defaultState="closed">
|
||||||
|
<ExplorerTree />
|
||||||
|
</CollapsibleTree>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/snippet}
|
||||||
|
</CollapsibleTreeNode>
|
||||||
|
</CollapsibleTree>
|
||||||
|
{/key}
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { CollapsibleTreeNode } from '$lib/components/collapsible-tree/index';
|
||||||
|
import { Button } from '$lib/components/ui/button';
|
||||||
|
import { FileText } from '@lucide/svelte';
|
||||||
|
import ExplorerTree from './ExplorerTree.svelte';
|
||||||
|
import { openPath } from '$lib/actions/file-actions';
|
||||||
|
import { engine, type FolderEntry } from '$lib/engine';
|
||||||
|
import { selectedFileIds } from '$lib/engine/selection-helpers';
|
||||||
|
import { fileIdAt } from '$lib/engine/workspace';
|
||||||
|
|
||||||
|
let {
|
||||||
|
path,
|
||||||
|
}: {
|
||||||
|
/** The folder whose content is listed, the open folder if it is not given. */
|
||||||
|
path?: string;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
const { workspace, selection, folderVersion } = engine;
|
||||||
|
|
||||||
|
let entries: FolderEntry[] = $state([]);
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
// read again when something changed on the file system
|
||||||
|
$folderVersion;
|
||||||
|
let current = true;
|
||||||
|
engine.listFolder(path).then((list) => {
|
||||||
|
if (current) {
|
||||||
|
entries = list;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
current = false;
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Opens the file, or selects it if it is open already. */
|
||||||
|
function open(entry: FolderEntry, id: string | undefined) {
|
||||||
|
if (id !== undefined) {
|
||||||
|
engine.select([id]);
|
||||||
|
} else {
|
||||||
|
openPath(entry.path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#each entries as entry (entry.path)}
|
||||||
|
{#if entry.isDir}
|
||||||
|
<CollapsibleTreeNode id={entry.name}>
|
||||||
|
{#snippet trigger()}
|
||||||
|
<span class="grow truncate text-left">{entry.name}</span>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet content(opened)}
|
||||||
|
<!-- the content of a folder is only read when it is opened -->
|
||||||
|
{#if opened}
|
||||||
|
<div class="ml-4">
|
||||||
|
<ExplorerTree path={entry.path} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/snippet}
|
||||||
|
</CollapsibleTreeNode>
|
||||||
|
{:else}
|
||||||
|
{@const id = fileIdAt($workspace, entry.path)}
|
||||||
|
{@const selected = id !== undefined && selectedFileIds($selection).includes(id)}
|
||||||
|
{@const dirty = id !== undefined && $workspace?.files[id]?.dirty === true}
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
class="w-full h-7 px-1 justify-start gap-1.5 border-none font-normal {selected
|
||||||
|
? 'bg-accent'
|
||||||
|
: ''} {id === undefined ? 'text-muted-foreground' : ''}"
|
||||||
|
title={entry.path}
|
||||||
|
onclick={() => open(entry, id)}
|
||||||
|
>
|
||||||
|
<FileText size="16" class="shrink-0" />
|
||||||
|
<span class="grow truncate text-left">{entry.name}</span>
|
||||||
|
{#if dirty}
|
||||||
|
<span class="size-1.5 shrink-0 rounded-full bg-foreground"></span>
|
||||||
|
{/if}
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default as Explorer } from './Explorer.svelte';
|
||||||
@@ -36,14 +36,14 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ScrollArea
|
<ScrollArea
|
||||||
class="shrink-0 {orientation === 'vertical' ? 'p-0 pr-3' : 'h-10 px-1'}"
|
class="shrink-0 {orientation === 'vertical' ? 'p-0' : 'h-10 px-1'}"
|
||||||
{orientation}
|
{orientation}
|
||||||
scrollbarXClasses={orientation === 'vertical' ? '' : 'hidden'}
|
scrollbarXClasses={orientation === 'vertical' ? '' : 'hidden'}
|
||||||
scrollbarYClasses={orientation === 'vertical' ? '' : ''}
|
scrollbarYClasses={orientation === 'vertical' ? '' : ''}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="flex {orientation === 'vertical'
|
class="flex {orientation === 'vertical'
|
||||||
? 'flex-col py-1 pl-1 min-h-screen'
|
? 'flex-col py-1 pl-1 pr-2 min-h-screen'
|
||||||
: 'flex-row'} {className ?? ''}"
|
: 'flex-row'} {className ?? ''}"
|
||||||
{style}
|
{style}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -32,6 +32,14 @@ export class SortableFileList {
|
|||||||
private _sortable: Sortable | null = null;
|
private _sortable: Sortable | null = null;
|
||||||
private _elements: { [id: string]: HTMLElement } = {};
|
private _elements: { [id: string]: HTMLElement } = {};
|
||||||
private _updatingSelection: boolean = false;
|
private _updatingSelection: boolean = false;
|
||||||
|
/**
|
||||||
|
* Whether a change of the selection that this list made is being applied by the engine. A click
|
||||||
|
* makes several events, handled after one another, and the engine can answer after them (in
|
||||||
|
* the desktop app, it is in another process): until it has, the list holds the selection, and
|
||||||
|
* the engine and the list disagree because the engine is behind, not because the user did
|
||||||
|
* something else.
|
||||||
|
*/
|
||||||
|
private _applying: boolean = false;
|
||||||
/** The elements of the list when a drag from it started, to put them back when it ends. */
|
/** The elements of the list when a drag from it started, to put them back when it ends. */
|
||||||
private _snapshot: ChildNode[] = [];
|
private _snapshot: ChildNode[] = [];
|
||||||
private _unsubscribes: (() => void)[] = [];
|
private _unsubscribes: (() => void)[] = [];
|
||||||
@@ -196,6 +204,9 @@ export class SortableFileList {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateFromSelection() {
|
updateFromSelection() {
|
||||||
|
if (this._applying) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const changed = this.getChangedIds();
|
const changed = this.getChangedIds();
|
||||||
if (changed.length === 0) {
|
if (changed.length === 0) {
|
||||||
return;
|
return;
|
||||||
@@ -220,7 +231,7 @@ export class SortableFileList {
|
|||||||
|
|
||||||
updateToSelection(e: Sortable.SortableEvent) {
|
updateToSelection(e: Sortable.SortableEvent) {
|
||||||
if (!this._sortable) return;
|
if (!this._sortable) return;
|
||||||
if (this._updatingSelection) return;
|
if (this._updatingSelection || this._applying) return;
|
||||||
this._updatingSelection = true;
|
this._updatingSelection = true;
|
||||||
const changed = this.getChangedIds();
|
const changed = this.getChangedIds();
|
||||||
if (changed.length == 0) {
|
if (changed.length == 0) {
|
||||||
@@ -242,28 +253,41 @@ export class SortableFileList {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const parent = this._parent;
|
const parent = this._parent;
|
||||||
|
let request: Promise<boolean>;
|
||||||
if (selected.length === 0 || !this.childNode(selected[0])) {
|
if (selected.length === 0 || !this.childNode(selected[0])) {
|
||||||
engine.select([]);
|
request = engine.select([]);
|
||||||
} else {
|
} else {
|
||||||
switch (this._sortableLevel) {
|
switch (this._sortableLevel) {
|
||||||
case 'file':
|
case 'file':
|
||||||
engine.select(selected);
|
request = engine.select(selected);
|
||||||
break;
|
break;
|
||||||
case 'track':
|
case 'track':
|
||||||
engine.selectTracks(parent!.fileId, selected);
|
request = engine.selectTracks(parent!.fileId, selected);
|
||||||
break;
|
break;
|
||||||
case 'segment':
|
case 'segment':
|
||||||
engine.selectSegments(parent!.fileId, (parent as any).trackId, selected);
|
request = engine.selectSegments(
|
||||||
|
parent!.fileId,
|
||||||
|
(parent as any).trackId,
|
||||||
|
selected
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
case 'waypoints':
|
case 'waypoints':
|
||||||
engine.selectWaypointGroup(parent!.fileId);
|
request = engine.selectWaypointGroup(parent!.fileId);
|
||||||
break;
|
break;
|
||||||
case 'waypoint':
|
case 'waypoint':
|
||||||
engine.selectWaypoints(parent!.fileId, selected);
|
request = engine.selectWaypoints(parent!.fileId, selected);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this._updatingSelection = false;
|
this._updatingSelection = false;
|
||||||
|
|
||||||
|
// the events of the same click that come before the engine answers are not new choices
|
||||||
|
this._applying = true;
|
||||||
|
request.finally(() => {
|
||||||
|
this._applying = false;
|
||||||
|
// the list shows what the engine selected
|
||||||
|
this.updateFromSelection();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
updateFromFileOrder() {
|
updateFromFileOrder() {
|
||||||
|
|||||||
@@ -221,12 +221,13 @@ export async function createTauriBackend(): Promise<Backend> {
|
|||||||
exportFiles: (fileIds, options) =>
|
exportFiles: (fileIds, options) =>
|
||||||
invoke<boolean>('export_files', { fileIds, options }),
|
invoke<boolean>('export_files', { fileIds, options }),
|
||||||
listFolder: (path) => invoke<FolderEntry[]>('list_folder', { path }),
|
listFolder: (path) => invoke<FolderEntry[]>('list_folder', { path }),
|
||||||
listen: async (onEngineChanged, onWorkspaceChanged, onFilesOpened) => {
|
listen: async (onEngineChanged, onWorkspaceChanged, onFilesOpened, onFolderChanged) => {
|
||||||
await listen<Outcome>('files-opened', (event) => onFilesOpened(event.payload));
|
await listen<Outcome>('files-opened', (event) => onFilesOpened(event.payload));
|
||||||
await listen<Outcome>('engine-changed', (event) => onEngineChanged(event.payload));
|
await listen<Outcome>('engine-changed', (event) => onEngineChanged(event.payload));
|
||||||
await listen<WorkspaceState>('workspace-changed', (event) =>
|
await listen<WorkspaceState>('workspace-changed', (event) =>
|
||||||
onWorkspaceChanged(event.payload)
|
onWorkspaceChanged(event.payload)
|
||||||
);
|
);
|
||||||
|
await listen<string[]>('folder-changed', () => onFolderChanged());
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -91,13 +91,15 @@ export interface DesktopFiles {
|
|||||||
listFolder(path?: string): Promise<FolderEntry[]>;
|
listFolder(path?: string): Promise<FolderEntry[]>;
|
||||||
/**
|
/**
|
||||||
* Tells when the engine changed without being asked, because the files on the disk did (the
|
* 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
|
* outcome to apply), when the status of the files changed, when files were opened with the
|
||||||
* app from the system (the outcome to apply, which selected them).
|
* app from the system (the outcome to apply, which selected them), and when something changed
|
||||||
|
* in the open folder (the tree of its files is to be read again).
|
||||||
*/
|
*/
|
||||||
listen(
|
listen(
|
||||||
onEngineChanged: (outcome: Outcome) => void,
|
onEngineChanged: (outcome: Outcome) => void,
|
||||||
onWorkspaceChanged: (workspace: WorkspaceState) => void,
|
onWorkspaceChanged: (workspace: WorkspaceState) => void,
|
||||||
onFilesOpened: (outcome: Outcome) => void
|
onFilesOpened: (outcome: Outcome) => void,
|
||||||
|
onFolderChanged: () => void
|
||||||
): Promise<void>;
|
): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ class Engine {
|
|||||||
private _lastError = writable<string | undefined>(undefined);
|
private _lastError = writable<string | undefined>(undefined);
|
||||||
private _outdated = writable(false);
|
private _outdated = writable(false);
|
||||||
private _workspace = writable<WorkspaceState | undefined>(undefined);
|
private _workspace = writable<WorkspaceState | undefined>(undefined);
|
||||||
|
private _folderVersion = writable(0);
|
||||||
private _canUndo = writable(false);
|
private _canUndo = writable(false);
|
||||||
private _canRedo = writable(false);
|
private _canRedo = writable(false);
|
||||||
private _canPaste = writable(false);
|
private _canPaste = writable(false);
|
||||||
@@ -86,6 +87,11 @@ class Engine {
|
|||||||
readonly workspace: Readable<WorkspaceState | undefined> = {
|
readonly workspace: Readable<WorkspaceState | undefined> = {
|
||||||
subscribe: this._workspace.subscribe,
|
subscribe: this._workspace.subscribe,
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
* A number that changes when something changed in the open folder on the file system, to read
|
||||||
|
* its tree again.
|
||||||
|
*/
|
||||||
|
readonly folderVersion: Readable<number> = { subscribe: this._folderVersion.subscribe };
|
||||||
/** Why the last call to the engine did nothing, `undefined` if it did something. */
|
/** Why the last call to the engine did nothing, `undefined` if it did something. */
|
||||||
readonly lastError: Readable<string | undefined> = { subscribe: this._lastError.subscribe };
|
readonly lastError: Readable<string | undefined> = { subscribe: this._lastError.subscribe };
|
||||||
/** What was copied or cut, waiting to be pasted. */
|
/** What was copied or cut, waiting to be pasted. */
|
||||||
@@ -689,7 +695,8 @@ class Engine {
|
|||||||
await this.sync(backend, outcome);
|
await this.sync(backend, outcome);
|
||||||
this.report(outcome);
|
this.report(outcome);
|
||||||
this._opened.forEach((callback) => callback(outcome.added));
|
this._opened.forEach((callback) => callback(outcome.added));
|
||||||
})
|
}),
|
||||||
|
() => this._folderVersion.update((version) => version + 1)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { describe, it } from 'node:test';
|
import { describe, it } from 'node:test';
|
||||||
import { folderName, subfolder } from '$lib/engine/workspace';
|
import { fileIdAt, folderName, subfolder } from '$lib/engine/workspace';
|
||||||
|
|
||||||
describe('subfolder', () => {
|
describe('subfolder', () => {
|
||||||
it('is where a file is in the folder', () => {
|
it('is where a file is in the folder', () => {
|
||||||
@@ -27,3 +27,23 @@ describe('folderName', () => {
|
|||||||
assert.equal(folderName('C:\\Users\\me\\tracks'), 'tracks');
|
assert.equal(folderName('C:\\Users\\me\\tracks'), 'tracks');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('fileIdAt', () => {
|
||||||
|
const workspace = {
|
||||||
|
files: {
|
||||||
|
a: { path: '/tracks/a.gpx' },
|
||||||
|
b: { path: '/tracks/sub/b.gpx' },
|
||||||
|
c: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
it('is the open file that is linked to the path', () => {
|
||||||
|
assert.equal(fileIdAt(workspace, '/tracks/a.gpx'), 'a');
|
||||||
|
assert.equal(fileIdAt(workspace, '/tracks/sub/b.gpx'), 'b');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is nothing for a file that is not open', () => {
|
||||||
|
assert.equal(fileIdAt(workspace, '/tracks/other.gpx'), undefined);
|
||||||
|
assert.equal(fileIdAt(undefined, '/tracks/a.gpx'), undefined);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -26,3 +26,11 @@ export function subfolder(
|
|||||||
}
|
}
|
||||||
return file.slice(root.length, -1).join('/');
|
return file.slice(root.length, -1).join('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The id of the open file that is linked to the file at `path`, `undefined` if there is none. */
|
||||||
|
export function fileIdAt(
|
||||||
|
workspace: { files: Record<string, { path?: string }> } | undefined,
|
||||||
|
path: string
|
||||||
|
): string | undefined {
|
||||||
|
return Object.entries(workspace?.files ?? {}).find(([, file]) => file.path === path)?.[0];
|
||||||
|
}
|
||||||
@@ -156,6 +156,7 @@ export const settings = {
|
|||||||
defaultWidth: new Setting('defaultWidth', browser && window.innerWidth < 600 ? 8 : 5),
|
defaultWidth: new Setting('defaultWidth', browser && window.innerWidth < 600 ? 8 : 5),
|
||||||
bottomPanelSize: new Setting('bottomPanelSize', 170),
|
bottomPanelSize: new Setting('bottomPanelSize', 170),
|
||||||
rightPanelSize: new Setting('rightPanelSize', 240),
|
rightPanelSize: new Setting('rightPanelSize', 240),
|
||||||
|
explorerPanelSize: new Setting('explorerPanelSize', 0),
|
||||||
/** Takes the saved values (see `Engine.openStorage`). */
|
/** Takes the saved values (see `Engine.openStorage`). */
|
||||||
connect(stored: StoredSettings) {
|
connect(stored: StoredSettings) {
|
||||||
for (const key in settings) {
|
for (const key in settings) {
|
||||||
|
|||||||
@@ -14,6 +14,8 @@
|
|||||||
"new_segment": "New segment",
|
"new_segment": "New segment",
|
||||||
"open": "Open...",
|
"open": "Open...",
|
||||||
"open_folder": "Open folder...",
|
"open_folder": "Open folder...",
|
||||||
|
"explorer": "Explorer",
|
||||||
|
"workspace": "Workspace",
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
"save_all": "Save all",
|
"save_all": "Save all",
|
||||||
"save_as": "Save as...",
|
"save_as": "Save as...",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import GPXLayers from '$lib/components/map/gpx-layer/GPXLayers.svelte';
|
import GPXLayers from '$lib/components/map/gpx-layer/GPXLayers.svelte';
|
||||||
import ElevationProfile from '$lib/components/elevation-profile/ElevationProfile.svelte';
|
import ElevationProfile from '$lib/components/elevation-profile/ElevationProfile.svelte';
|
||||||
import FileList from '$lib/components/file-list/FileList.svelte';
|
import FileList from '$lib/components/file-list/FileList.svelte';
|
||||||
|
import Explorer from '$lib/components/explorer/Explorer.svelte';
|
||||||
import GPXStatistics from '$lib/components/GPXStatistics.svelte';
|
import GPXStatistics from '$lib/components/GPXStatistics.svelte';
|
||||||
import Map from '$lib/components/map/Map.svelte';
|
import Map from '$lib/components/map/Map.svelte';
|
||||||
import Menu from '$lib/components/Menu.svelte';
|
import Menu from '$lib/components/Menu.svelte';
|
||||||
@@ -23,17 +24,29 @@
|
|||||||
import { getURLForGoogleDriveFile } from '$lib/components/embedding/embedding';
|
import { getURLForGoogleDriveFile } from '$lib/components/embedding/embedding';
|
||||||
|
|
||||||
const outdated = engine.outdated;
|
const outdated = engine.outdated;
|
||||||
|
const workspace = engine.workspace;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
treeFileView,
|
treeFileView,
|
||||||
elevationProfile,
|
elevationProfile,
|
||||||
bottomPanelSize,
|
bottomPanelSize,
|
||||||
rightPanelSize,
|
rightPanelSize,
|
||||||
|
explorerPanelSize,
|
||||||
additionalDatasets,
|
additionalDatasets,
|
||||||
elevationFill,
|
elevationFill,
|
||||||
} = settings;
|
} = settings;
|
||||||
|
|
||||||
let bottomPanelWidth: number | undefined = $state();
|
let bottomPanelWidth: number | undefined = $state();
|
||||||
|
// the file list and the explorer share the right panel, half of it each until it is resized
|
||||||
|
let rightPanelHeight = $state(0);
|
||||||
|
let explorerHeight = $derived(
|
||||||
|
$treeFileView
|
||||||
|
? Math.min(
|
||||||
|
Math.max($explorerPanelSize > 0 ? $explorerPanelSize : rightPanelHeight / 2, 60),
|
||||||
|
Math.max(rightPanelHeight - 80, 60)
|
||||||
|
)
|
||||||
|
: rightPanelHeight
|
||||||
|
);
|
||||||
let bottomPanelOrientation = $derived(
|
let bottomPanelOrientation = $derived(
|
||||||
bottomPanelWidth && bottomPanelWidth >= 540 && $elevationProfile ? 'horizontal' : 'vertical'
|
bottomPanelWidth && bottomPanelWidth >= 540 && $elevationProfile ? 'horizontal' : 'vertical'
|
||||||
);
|
);
|
||||||
@@ -153,9 +166,43 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{#if $treeFileView}
|
{#if $treeFileView || $workspace?.folder}
|
||||||
<Resizer orientation="col" bind:after={$rightPanelSize} minAfter={100} maxAfter={400} />
|
<Resizer orientation="col" bind:after={$rightPanelSize} minAfter={100} maxAfter={400} />
|
||||||
<FileList orientation="vertical" recursive={true} style="width: {$rightPanelSize}px" />
|
<div
|
||||||
|
bind:clientHeight={rightPanelHeight}
|
||||||
|
class="flex flex-col h-full min-h-0 shrink-0"
|
||||||
|
style="width: {$rightPanelSize}px"
|
||||||
|
>
|
||||||
|
{#if $treeFileView}
|
||||||
|
{#if $workspace}
|
||||||
|
<div class="px-2 py-1 text-sm font-semibold shrink-0">
|
||||||
|
{i18n._('menu.workspace')}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<!-- a row, as the file list was: its scroll area is as high as this, whatever its content -->
|
||||||
|
<div class="grow min-h-0 flex flex-row">
|
||||||
|
<FileList
|
||||||
|
orientation="vertical"
|
||||||
|
recursive={true}
|
||||||
|
style="width: {$rightPanelSize}px"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<!-- the open folder (the desktop app) under the files that are open -->
|
||||||
|
{#if $workspace?.folder}
|
||||||
|
{#if $treeFileView}
|
||||||
|
<Resizer
|
||||||
|
orientation="row"
|
||||||
|
bind:after={() => explorerHeight, (height) => ($explorerPanelSize = height)}
|
||||||
|
minAfter={60}
|
||||||
|
maxAfter={Math.max(rightPanelHeight - 80, 60)}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
<div class="flex flex-col min-h-0 shrink-0" style="height: {explorerHeight}px">
|
||||||
|
<Explorer />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user