diff --git a/tauri/src/commands/disk.rs b/tauri/src/commands/disk.rs index b46ea489a..ad6cb0f82 100644 --- a/tauri/src/commands/disk.rs +++ b/tauri/src/commands/disk.rs @@ -88,6 +88,16 @@ pub fn open_paths( }) } +/// The files are open for good: the preview among them is not a preview anymore (the user did +/// something to it that the engine does not see, like hiding it). +#[tauri::command(async)] +pub fn keep_files(state: State, file_ids: Vec) { + state.core(move |core| { + core.workspace.keep(&file_ids); + core.commit(None); + }) +} + /// Writes the files among `ids` that are not the ones on the disk, asking where to put the ones /// that have no place: the file dialog for a single one, a folder for several of them. Says whether /// they were written, which is not the case if the user cancelled. diff --git a/tauri/src/lib.rs b/tauri/src/lib.rs index cd5900ba1..b40606759 100644 --- a/tauri/src/lib.rs +++ b/tauri/src/lib.rs @@ -186,6 +186,7 @@ fn register_commands(builder: tauri::Builder) -> tauri::Bu disk::workspace_state, disk::open_files, disk::open_paths, + disk::keep_files, disk::open_folder, disk::close_folder, disk::close_files, diff --git a/tauri/src/state.rs b/tauri/src/state.rs index 1f30ae119..41b60c4e2 100644 --- a/tauri/src/state.rs +++ b/tauri/src/state.rs @@ -108,8 +108,10 @@ impl Core { self.workspace.keep(&[id]); } let (renamed, errors) = self.workspace.follow_names(&mut self.engine, &names); - if kept || renamed { - self.commit(None); + // and it goes when something else is selected + let dropped = self.workspace.drop_preview(&mut self.engine); + if kept || renamed || dropped.is_some() { + self.commit(dropped.as_ref()); } else { self.persist(); } diff --git a/tauri/src/tests.rs b/tauri/src/tests.rs index 91236e448..9f26488f8 100644 --- a/tauri/src/tests.rs +++ b/tauri/src/tests.rs @@ -725,3 +725,43 @@ fn a_file_open_as_a_preview_is_replaced_until_it_is_kept_or_edited() { ); std::fs::remove_dir_all(folder).unwrap(); } + +#[test] +fn the_preview_goes_when_something_else_is_selected_unless_it_was_kept() { + let app = App::new(); + let folder = folder_with(&["a", "b", "c"]); + let open = |name: &str, preview: bool| { + app.call( + "open_paths", + json!({ "paths": [folder.join(format!("{name}.gpx"))], "preview": preview }), + )["added"][0] + .as_str() + .map(str::to_string) + }; + let order = |app: &App| app.call("file_order", json!({})).as_array().unwrap().len(); + let a = open("a", false).unwrap(); + + // looked at, then something else is selected: it goes + let b = open("b", true).unwrap(); + assert_eq!(order(&app), 2); + app.call("select", json!({ "fileIds": [a], "mode": "replace" })); + assert_eq!(order(&app), 1); + + // nothing selected: it stays + open("b", true); + app.call("select", json!({ "fileIds": [], "mode": "replace" })); + assert_eq!(order(&app), 2); + + // kept (hidden, its menu opened...): it stays when something else is selected + let c = open("c", true).unwrap(); + assert_eq!(order(&app), 2, "c replaces b"); + app.call("keep_files", json!({ "fileIds": [c] })); + assert_eq!( + app.call("workspace_state", json!({}))["files"][&c]["preview"], + false + ); + app.call("select", json!({ "fileIds": [a], "mode": "replace" })); + assert_eq!(order(&app), 2); + let _ = b; + std::fs::remove_dir_all(folder).unwrap(); +} diff --git a/tauri/src/workspace.rs b/tauri/src/workspace.rs index 220294288..0bd66fc3c 100644 --- a/tauri/src/workspace.rs +++ b/tauri/src/workspace.rs @@ -450,6 +450,17 @@ impl Workspace { closed.merge(opened) } + /// Closes the preview when something else is selected (not nothing, nor something in it): it was + /// only looked at. Says what changed in the engine, `None` if it stays. + pub fn drop_preview(&mut self, engine: &mut Engine) -> Option { + let id = self.preview?; + let selected = engine.selection().file_ids(); + if selected.is_empty() || selected.contains(&FileId(id)) { + return None; + } + Some(self.close_files(engine, &[id], false)) + } + /// The files are open for good: none of them is the preview anymore. pub fn keep(&mut self, ids: &[Uuid]) { if self.preview.is_some_and(|preview| ids.contains(&preview)) { diff --git a/website/src/lib/components/explorer/ExplorerRow.svelte b/website/src/lib/components/explorer/ExplorerRow.svelte index 6a2142e71..936163422 100644 --- a/website/src/lib/components/explorer/ExplorerRow.svelte +++ b/website/src/lib/components/explorer/ExplorerRow.svelte @@ -228,6 +228,10 @@ { + if (open && fileId !== undefined) { + // the file is open for good once its menu is + engine.keepPreview([fileId]); + } if (open && !selected && kind !== 'root') { select([path]); } diff --git a/website/src/lib/components/file-list/FileListNodeLabel.svelte b/website/src/lib/components/file-list/FileListNodeLabel.svelte index 6eabde209..46c19fc98 100644 --- a/website/src/lib/components/file-list/FileListNodeLabel.svelte +++ b/website/src/lib/components/file-list/FileListNodeLabel.svelte @@ -19,7 +19,7 @@ FileStack, FileX, } from '@lucide/svelte'; - import { getContext } from 'svelte'; + import { getContext, untrack } from 'svelte'; import { i18n } from '$lib/i18n/i18n.svelte'; import MetadataDialog from '$lib/components/file-list/metadata/MetadataDialog.svelte'; import { editMetadata } from '$lib/components/file-list/metadata/utils.svelte'; @@ -94,6 +94,12 @@ selected && selectedElementIds($selection)[0]?.ids[0] === elementId(node) ); + $effect(() => { + // the file is open for good once its info or its appearance is (the desktop app) + if (openEditMetadata || openEditStyle) { + untrack(() => engine.keepPreview([node.fileId])); + } + }); // the desktop app: the files are linked to the file system, they are closed rather than deleted let desktop = $derived($workspace !== undefined); @@ -114,6 +120,8 @@ { if (open) { + // the file is open for good once its menu is (the desktop app) + engine.keepPreview([node.fileId]); if (!selected) { engine.selectNode(node); } diff --git a/website/src/lib/components/file-list/sortable-file-list.ts b/website/src/lib/components/file-list/sortable-file-list.ts index e73d6acbe..bce2a2737 100644 --- a/website/src/lib/components/file-list/sortable-file-list.ts +++ b/website/src/lib/components/file-list/sortable-file-list.ts @@ -69,10 +69,12 @@ export class SortableFileList { setTimeout(() => this.updateToSelection(e), 50), onDeselect: (e: Sortable.SortableEvent) => setTimeout(() => this.updateToSelection(e), 50), - onStart: () => { + onStart: (e: Sortable.SortableEvent) => { // all the nodes, not only the elements: the comments are the anchors of the Svelte blocks this._snapshot = Array.from(container.childNodes); dragging.set(sortableLevel); + // the files that are dragged (or whose elements are) are open for good + engine.keepPreview(this.draggedFiles(e)); }, onEnd: () => dragging.set(null), onSort: (e: Sortable.SortableEvent) => this.onSort(e), @@ -84,6 +86,17 @@ export class SortableFileList { ); } + /** The files of what is dragged: the dragged files, or the file of the dragged elements. */ + private draggedFiles(e: Sortable.SortableEvent): string[] { + if (this._parent) { + return [this._parent.fileId]; + } + const elements: HTMLElement[] = e.items.length > 0 ? e.items : [e.item]; + return elements + .map((element) => element.getAttribute('data-id')) + .filter((id): id is string => id !== null); + } + /** The node of a child of the list. */ childNode(id: string): FileTreeNode | undefined { const parent = this._parent; diff --git a/website/src/lib/engine/backend/tauri.ts b/website/src/lib/engine/backend/tauri.ts index 977614fb0..4655dbefd 100644 --- a/website/src/lib/engine/backend/tauri.ts +++ b/website/src/lib/engine/backend/tauri.ts @@ -212,6 +212,7 @@ export async function createTauriBackend(): Promise { workspace: () => invoke('workspace_state'), openFiles: () => invoke('open_files'), openPaths: (paths, preview) => invoke('open_paths', { paths, preview }), + keepFiles: (fileIds) => invoke('keep_files', { fileIds }), openFolder: (unsaved) => invoke('open_folder', { unsaved }), closeFolder: (unsaved) => invoke('close_folder', { unsaved }), closeFiles: (fileIds, unsaved) => invoke('close_files', { fileIds, unsaved }), diff --git a/website/src/lib/engine/backend/types.ts b/website/src/lib/engine/backend/types.ts index f56a965f1..91822e1c1 100644 --- a/website/src/lib/engine/backend/types.ts +++ b/website/src/lib/engine/backend/types.ts @@ -64,6 +64,8 @@ export interface DesktopFiles { * edited or opened for good. Otherwise they are open for good. */ openPaths(paths: string[], preview?: boolean): Promise; + /** The files are open for good: the preview among them is not a preview anymore. */ + keepFiles(fileIds: string[]): Promise; /** * 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 diff --git a/website/src/lib/engine/engine.ts b/website/src/lib/engine/engine.ts index c147ee6e3..9a33940de 100644 --- a/website/src/lib/engine/engine.ts +++ b/website/src/lib/engine/engine.ts @@ -570,6 +570,18 @@ class Engine { return this.addedBy((b) => b.desktop!.openPaths(paths, preview)); } + /** + * The files are open for good (the desktop app): the preview among them stays when another file + * is opened or selected. The user did something to it, which the engine does not see (opening + * its menu or its info, hiding it, dragging it). Nothing is done for the other files. + */ + async keepPreview(fileIds: string[]) { + const workspace = get(this._workspace); + if (fileIds.some((id) => workspace?.files[id]?.preview)) { + await this.desktopCall((desktop) => desktop.keepFiles(fileIds)); + } + } + // What the explorer does to the tree of the open folder (the desktop app). When it fails, // `lastError` tells why (a key of the dictionaries, or a message). @@ -800,6 +812,7 @@ class Engine { if (!store) { return; } + void this.keepPreview([fileId]); const visibility = setHidden( get(store).structure, this._visibility.get(fileId) ?? new Map(),