mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2026-10-19 09:22:12 +00:00
progress
This commit is contained in:
1 parent
ed90ba6a1f
commit
b2495d1466
6 files changed
+186
-16
No files matched your search
@@ -348,15 +348,12 @@ pub fn export_files<R: Runtime>(
|
||||
options: ExportArguments,
|
||||
) -> Result<bool, String> {
|
||||
let options = ExportOptions::from(options);
|
||||
let files: Vec<(String, Vec<u8>)> = state.read(move |engine| {
|
||||
let files: Vec<(String, Vec<u8>)> = state.core(move |core| {
|
||||
file_ids
|
||||
.iter()
|
||||
.filter_map(|id| {
|
||||
let name = engine
|
||||
.file_structure(&gpx_engine::FileId(*id))?
|
||||
.name
|
||||
.clone();
|
||||
Some((name, api::export_file(engine, *id, options)?))
|
||||
let name = core.workspace.display_name(&core.engine, *id)?;
|
||||
Some((name, api::export_file(&core.engine, *id, options)?))
|
||||
})
|
||||
.collect()
|
||||
});
|
||||
|
||||
+30
-1
@@ -201,9 +201,38 @@ impl Core {
|
||||
.then(|| self.workspace.folder.clone())
|
||||
.flatten()
|
||||
});
|
||||
let outcome = f(&mut self.engine);
|
||||
// what the files it comes from are called, by the name in them (the copies have the same one)
|
||||
let sources: Vec<(String, String)> = selected
|
||||
.iter()
|
||||
.filter_map(|id| {
|
||||
let structure = self.engine.file_structure(&gpx_engine::FileId(*id))?;
|
||||
Some((
|
||||
structure.name.clone(),
|
||||
self.workspace.display_name(&self.engine, *id)?,
|
||||
))
|
||||
})
|
||||
.collect();
|
||||
let mut outcome = f(&mut self.engine);
|
||||
if let Some(dir) = dir {
|
||||
self.workspace.inherit(outcome.added(), &dir);
|
||||
// what is made in a folder is written there at once, so that the tree shows it. A copy
|
||||
// is named like the file it is a copy of
|
||||
let made: Vec<(uuid::Uuid, Option<String>)> = outcome
|
||||
.added()
|
||||
.iter()
|
||||
.filter_map(|id| uuid::Uuid::parse_str(id).ok())
|
||||
.filter(|id| self.workspace.dirs.contains_key(id))
|
||||
.filter_map(|id| {
|
||||
let name = &self.engine.file_structure(&gpx_engine::FileId(id))?.name;
|
||||
let source = sources.iter().find(|(source, _)| source == name);
|
||||
Some((id, source.map(|(_, display)| display.clone())))
|
||||
})
|
||||
.collect();
|
||||
if let Err(error) = self.workspace.save_new(&self.engine, &made)
|
||||
&& !outcome.has_error()
|
||||
{
|
||||
outcome = outcome.merge(api::Outcome::failed(error));
|
||||
}
|
||||
}
|
||||
self.keep_workspace();
|
||||
self.persist();
|
||||
|
||||
+80
-4
@@ -380,21 +380,23 @@ fn files_are_opened_closed_and_made_in_a_folder() {
|
||||
assert_eq!(state["files"][&id]["dirty"], false);
|
||||
assert!(state["files"][&id]["path"].is_string());
|
||||
|
||||
// a new file made in a folder is written there, and is not linked to a place yet
|
||||
// a new file made in a folder is written there at once, so that the tree shows it
|
||||
let created = app.call(
|
||||
"new_file",
|
||||
json!({ "name": "route", "lng": 4.4, "lat": 50.8, "folder": folder }),
|
||||
);
|
||||
let new_id = created["added"][0].as_str().unwrap().to_string();
|
||||
let state = app.call("workspace_state", json!({}));
|
||||
assert_eq!(state["files"][&new_id]["dirty"], true);
|
||||
assert!(state["files"][&new_id]["folder"].is_string());
|
||||
assert!(state["files"][&new_id].get("path").is_none());
|
||||
assert_eq!(state["files"][&new_id]["dirty"], false);
|
||||
assert!(state["files"][&new_id]["path"].is_string());
|
||||
assert!(folder.join("route.gpx").exists());
|
||||
|
||||
// the tree needs an open folder
|
||||
assert_eq!(app.call("list_folder", json!({})), json!([]));
|
||||
|
||||
// closing: what is saved goes, what has edits stays unless it is discarded
|
||||
app.call("select", json!({ "fileIds": [new_id], "mode": "replace" }));
|
||||
app.call("metadata", json!({ "name": "edited", "desc": "" }));
|
||||
app.call("close_files", json!({ "fileIds": [id, new_id] }));
|
||||
assert_eq!(app.call("file_order", json!({})), json!([new_id]));
|
||||
app.call(
|
||||
@@ -455,3 +457,77 @@ fn a_file_outside_the_open_folder_is_read_again_when_the_disk_changes() {
|
||||
std::fs::remove_dir_all(folder).unwrap();
|
||||
std::fs::remove_dir_all(directory).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn files_made_from_a_file_of_the_open_folder_are_written_next_to_it_at_once() {
|
||||
let state = crate::state::AppState::temporary();
|
||||
let folder = std::env::temp_dir().join(format!("gpx-tests-{}", uuid::Uuid::new_v4()));
|
||||
std::fs::create_dir_all(folder.join("sub")).unwrap();
|
||||
std::fs::copy("../gpx-rs/data/simple.gpx", folder.join("sub/track.gpx")).unwrap();
|
||||
let sub = folder.join("sub");
|
||||
let (copied, new_file) = state.core(move |core| {
|
||||
core.workspace.open_folder(&mut core.engine, &folder, false);
|
||||
let opened = core
|
||||
.workspace
|
||||
.open_files(&mut core.engine, &[sub.join("track.gpx")], true);
|
||||
core.keep_workspace();
|
||||
assert_eq!(opened.added().len(), 1);
|
||||
let copy = core.creating(None, false, gpx_engine::api::duplicate);
|
||||
let fresh = core.creating(None, true, |e| {
|
||||
gpx_engine::api::new_file(e, "fresh", None, None, None)
|
||||
});
|
||||
// a copy of the file goes next to it, a new file with nothing selected in the selection
|
||||
// of the file that was just made
|
||||
(copy, fresh)
|
||||
});
|
||||
let sub = std::env::temp_dir();
|
||||
let _ = sub;
|
||||
assert_eq!(copied.added().len(), 1);
|
||||
assert_eq!(new_file.added().len(), 1);
|
||||
let status = state.core(|core| core.workspace_state());
|
||||
assert!(status.files.values().all(|file| !file.dirty));
|
||||
let paths: Vec<String> = status
|
||||
.files
|
||||
.values()
|
||||
.map(|file| file.path.clone().unwrap())
|
||||
.collect();
|
||||
// the copy is in the subfolder, named after the file
|
||||
assert!(
|
||||
paths.iter().any(|path| path.ends_with("sub/track (2).gpx")),
|
||||
"{paths:?}"
|
||||
);
|
||||
// the new file went where the selected file is
|
||||
assert!(
|
||||
paths.iter().any(|path| path.ends_with("sub/fresh.gpx")),
|
||||
"{paths:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_parts_of_a_split_are_numbered_from_one() {
|
||||
let state = crate::state::AppState::temporary();
|
||||
let folder = std::env::temp_dir().join(format!("gpx-tests-{}", uuid::Uuid::new_v4()));
|
||||
std::fs::create_dir_all(&folder).unwrap();
|
||||
std::fs::copy("../gpx-rs/data/simple.gpx", folder.join("track.gpx")).unwrap();
|
||||
let root = folder.clone();
|
||||
let outcome = state.core(move |core| {
|
||||
core.workspace.open_folder(&mut core.engine, &root, false);
|
||||
core.workspace
|
||||
.open_files(&mut core.engine, &[root.join("track.gpx")], true);
|
||||
core.keep_workspace();
|
||||
let revision = core.engine.routing().revision;
|
||||
core.creating(None, false, |e| {
|
||||
gpx_engine::api::split(e, revision, 2, gpx_engine::SplitType::Files)
|
||||
})
|
||||
});
|
||||
assert_eq!(outcome.added().len(), 2, "{:?}", outcome.added());
|
||||
let mut names: Vec<String> = std::fs::read_dir(&folder)
|
||||
.unwrap()
|
||||
.flatten()
|
||||
.map(|entry| entry.file_name().to_string_lossy().into_owned())
|
||||
.collect();
|
||||
names.sort();
|
||||
// the file stays, its two parts are numbered from 1
|
||||
assert_eq!(names, ["track (1).gpx", "track (2).gpx", "track.gpx"]);
|
||||
std::fs::remove_dir_all(folder).unwrap();
|
||||
}
|
||||
@@ -193,6 +193,16 @@ pub fn free_path(folder: &Path, name: &str, taken: impl Fn(&Path) -> bool) -> Pa
|
||||
path
|
||||
}
|
||||
|
||||
/// Where to put one of several files that are called the same in `folder`: `name (1)`, or else the
|
||||
/// first number that is free.
|
||||
pub fn numbered_path(folder: &Path, name: &str, taken: impl Fn(&Path) -> bool) -> PathBuf {
|
||||
let name = file_name(name);
|
||||
(1..)
|
||||
.map(|number| folder.join(format!("{name} ({number}).gpx")))
|
||||
.find(|path| !path.exists() && !taken(path))
|
||||
.expect("there are enough numbers")
|
||||
}
|
||||
|
||||
impl Workspace {
|
||||
pub fn path() -> &'static str {
|
||||
"workspace.json"
|
||||
@@ -595,6 +605,58 @@ impl Workspace {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Writes the new files among `files` in their folder, each under the name that is given (the
|
||||
/// name in the file when there is none). A name that is the one of something else is changed
|
||||
/// (`name (2)`). Files that would be called the same, like the parts of a split, are numbered
|
||||
/// from 1 (`name (1)`, `name (2)`).
|
||||
pub fn save_new(
|
||||
&mut self,
|
||||
engine: &Engine,
|
||||
files: &[(Uuid, Option<String>)],
|
||||
) -> Result<(), String> {
|
||||
let names: Vec<Option<(Uuid, PathBuf, String)>> = files
|
||||
.iter()
|
||||
.map(|(id, name)| {
|
||||
let dir = self.dirs.get(id)?.clone();
|
||||
let structure = engine.file_structure(&FileId(*id))?;
|
||||
Some((
|
||||
*id,
|
||||
dir,
|
||||
name.clone().unwrap_or_else(|| structure.name.clone()),
|
||||
))
|
||||
})
|
||||
.collect();
|
||||
for entry in &names {
|
||||
let Some((id, dir, name)) = entry else {
|
||||
continue;
|
||||
};
|
||||
let together = names
|
||||
.iter()
|
||||
.flatten()
|
||||
.filter(|(_, other_dir, other)| {
|
||||
other_dir == dir && file_name(other) == file_name(name)
|
||||
})
|
||||
.count()
|
||||
> 1;
|
||||
let path = if together {
|
||||
numbered_path(dir, name, |p| self.is_taken(p))
|
||||
} else {
|
||||
free_path(dir, name, |p| self.is_taken(p))
|
||||
};
|
||||
self.write_file(engine, *id, &path)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The name of a file as the user knows it: the name of the file on the disk, without its
|
||||
/// extension, or else the name in the file.
|
||||
pub fn display_name(&self, engine: &Engine, id: Uuid) -> Option<String> {
|
||||
match self.links.get(&id) {
|
||||
Some(link) => Some(stem(&link.path)),
|
||||
None => Some(engine.file_structure(&FileId(id))?.name.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Writes a file to a path of the user's choice, which is the place of the file from then on.
|
||||
pub fn save_as(&mut self, engine: &Engine, id: Uuid, path: &Path) -> Result<(), String> {
|
||||
if let Some(other) = self.id_of(&canonical(path))
|
||||
|
||||
Reference in new issue
Block a user