mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2026-10-19 09:22:12 +00:00
improved preview
This commit is contained in:
1 parent
b64c192c4f
commit
9033401094
11 files changed
+109
-4
No files matched your search
@@ -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<AppState>, file_ids: Vec<Uuid>) {
|
||||||
|
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
|
/// 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
|
/// 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.
|
/// they were written, which is not the case if the user cancelled.
|
||||||
|
|||||||
@@ -186,6 +186,7 @@ fn register_commands<R: tauri::Runtime>(builder: tauri::Builder<R>) -> tauri::Bu
|
|||||||
disk::workspace_state,
|
disk::workspace_state,
|
||||||
disk::open_files,
|
disk::open_files,
|
||||||
disk::open_paths,
|
disk::open_paths,
|
||||||
|
disk::keep_files,
|
||||||
disk::open_folder,
|
disk::open_folder,
|
||||||
disk::close_folder,
|
disk::close_folder,
|
||||||
disk::close_files,
|
disk::close_files,
|
||||||
|
|||||||
+4
-2
@@ -108,8 +108,10 @@ impl Core {
|
|||||||
self.workspace.keep(&[id]);
|
self.workspace.keep(&[id]);
|
||||||
}
|
}
|
||||||
let (renamed, errors) = self.workspace.follow_names(&mut self.engine, &names);
|
let (renamed, errors) = self.workspace.follow_names(&mut self.engine, &names);
|
||||||
if kept || renamed {
|
// and it goes when something else is selected
|
||||||
self.commit(None);
|
let dropped = self.workspace.drop_preview(&mut self.engine);
|
||||||
|
if kept || renamed || dropped.is_some() {
|
||||||
|
self.commit(dropped.as_ref());
|
||||||
} else {
|
} else {
|
||||||
self.persist();
|
self.persist();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
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();
|
||||||
|
}
|
||||||
@@ -450,6 +450,17 @@ impl Workspace {
|
|||||||
closed.merge(opened)
|
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<api::Outcome> {
|
||||||
|
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.
|
/// The files are open for good: none of them is the preview anymore.
|
||||||
pub fn keep(&mut self, ids: &[Uuid]) {
|
pub fn keep(&mut self, ids: &[Uuid]) {
|
||||||
if self.preview.is_some_and(|preview| ids.contains(&preview)) {
|
if self.preview.is_some_and(|preview| ids.contains(&preview)) {
|
||||||
|
|||||||
@@ -228,6 +228,10 @@
|
|||||||
|
|
||||||
<ContextMenu.Root
|
<ContextMenu.Root
|
||||||
onOpenChange={(open) => {
|
onOpenChange={(open) => {
|
||||||
|
if (open && fileId !== undefined) {
|
||||||
|
// the file is open for good once its menu is
|
||||||
|
engine.keepPreview([fileId]);
|
||||||
|
}
|
||||||
if (open && !selected && kind !== 'root') {
|
if (open && !selected && kind !== 'root') {
|
||||||
select([path]);
|
select([path]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
FileStack,
|
FileStack,
|
||||||
FileX,
|
FileX,
|
||||||
} from '@lucide/svelte';
|
} from '@lucide/svelte';
|
||||||
import { getContext } from 'svelte';
|
import { getContext, untrack } from 'svelte';
|
||||||
import { i18n } from '$lib/i18n/i18n.svelte';
|
import { i18n } from '$lib/i18n/i18n.svelte';
|
||||||
import MetadataDialog from '$lib/components/file-list/metadata/MetadataDialog.svelte';
|
import MetadataDialog from '$lib/components/file-list/metadata/MetadataDialog.svelte';
|
||||||
import { editMetadata } from '$lib/components/file-list/metadata/utils.svelte';
|
import { editMetadata } from '$lib/components/file-list/metadata/utils.svelte';
|
||||||
@@ -94,6 +94,12 @@
|
|||||||
selected &&
|
selected &&
|
||||||
selectedElementIds($selection)[0]?.ids[0] === elementId(node)
|
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
|
// the desktop app: the files are linked to the file system, they are closed rather than deleted
|
||||||
let desktop = $derived($workspace !== undefined);
|
let desktop = $derived($workspace !== undefined);
|
||||||
@@ -114,6 +120,8 @@
|
|||||||
<ContextMenu.Root
|
<ContextMenu.Root
|
||||||
onOpenChange={(open) => {
|
onOpenChange={(open) => {
|
||||||
if (open) {
|
if (open) {
|
||||||
|
// the file is open for good once its menu is (the desktop app)
|
||||||
|
engine.keepPreview([node.fileId]);
|
||||||
if (!selected) {
|
if (!selected) {
|
||||||
engine.selectNode(node);
|
engine.selectNode(node);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,10 +69,12 @@ export class SortableFileList {
|
|||||||
setTimeout(() => this.updateToSelection(e), 50),
|
setTimeout(() => this.updateToSelection(e), 50),
|
||||||
onDeselect: (e: Sortable.SortableEvent) =>
|
onDeselect: (e: Sortable.SortableEvent) =>
|
||||||
setTimeout(() => this.updateToSelection(e), 50),
|
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
|
// all the nodes, not only the elements: the comments are the anchors of the Svelte blocks
|
||||||
this._snapshot = Array.from(container.childNodes);
|
this._snapshot = Array.from(container.childNodes);
|
||||||
dragging.set(sortableLevel);
|
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),
|
onEnd: () => dragging.set(null),
|
||||||
onSort: (e: Sortable.SortableEvent) => this.onSort(e),
|
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. */
|
/** The node of a child of the list. */
|
||||||
childNode(id: string): FileTreeNode | undefined {
|
childNode(id: string): FileTreeNode | undefined {
|
||||||
const parent = this._parent;
|
const parent = this._parent;
|
||||||
|
|||||||
@@ -212,6 +212,7 @@ export async function createTauriBackend(): Promise<Backend> {
|
|||||||
workspace: () => invoke<WorkspaceState>('workspace_state'),
|
workspace: () => invoke<WorkspaceState>('workspace_state'),
|
||||||
openFiles: () => invoke<Outcome>('open_files'),
|
openFiles: () => invoke<Outcome>('open_files'),
|
||||||
openPaths: (paths, preview) => invoke<Outcome>('open_paths', { paths, preview }),
|
openPaths: (paths, preview) => invoke<Outcome>('open_paths', { paths, preview }),
|
||||||
|
keepFiles: (fileIds) => invoke('keep_files', { fileIds }),
|
||||||
openFolder: (unsaved) => invoke<Outcome>('open_folder', { unsaved }),
|
openFolder: (unsaved) => invoke<Outcome>('open_folder', { unsaved }),
|
||||||
closeFolder: (unsaved) => invoke<Outcome>('close_folder', { unsaved }),
|
closeFolder: (unsaved) => invoke<Outcome>('close_folder', { unsaved }),
|
||||||
closeFiles: (fileIds, unsaved) => invoke<Outcome>('close_files', { fileIds, unsaved }),
|
closeFiles: (fileIds, unsaved) => invoke<Outcome>('close_files', { fileIds, unsaved }),
|
||||||
|
|||||||
@@ -64,6 +64,8 @@ export interface DesktopFiles {
|
|||||||
* edited or opened for good. Otherwise they are open for good.
|
* edited or opened for good. Otherwise they are open for good.
|
||||||
*/
|
*/
|
||||||
openPaths(paths: string[], preview?: boolean): Promise<Outcome>;
|
openPaths(paths: string[], preview?: boolean): Promise<Outcome>;
|
||||||
|
/** The files are open for good: the preview among them is not a preview anymore. */
|
||||||
|
keepFiles(fileIds: string[]): Promise<void>;
|
||||||
/**
|
/**
|
||||||
* Opens the folder that the user picks, which is watched. Its files are not opened: they are
|
* 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
|
* in its tree (`listFolder`). The folder that was open is closed with its files (see
|
||||||
|
|||||||
@@ -570,6 +570,18 @@ class Engine {
|
|||||||
return this.addedBy((b) => b.desktop!.openPaths(paths, preview));
|
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,
|
// 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).
|
// `lastError` tells why (a key of the dictionaries, or a message).
|
||||||
|
|
||||||
@@ -800,6 +812,7 @@ class Engine {
|
|||||||
if (!store) {
|
if (!store) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
void this.keepPreview([fileId]);
|
||||||
const visibility = setHidden(
|
const visibility = setHidden(
|
||||||
get(store).structure,
|
get(store).structure,
|
||||||
this._visibility.get(fileId) ?? new Map(),
|
this._visibility.get(fileId) ?? new Map(),
|
||||||
|
|||||||
Reference in new issue
Block a user