mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2026-10-28 02:47:17 +00:00
clarify name handling
This commit is contained in:
1 parent
45f877ec78
commit
3f8e2a0c56
14 files changed
+204
-251
No files matched your search
@@ -205,8 +205,8 @@ pub fn load_files<H: History>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Brings changes of the files from the outside in (see [`engine::SyncFiles`]): each file is
|
/// Brings changes of the files from the outside in (see [`engine::SyncFiles`]): each file is
|
||||||
/// `(id, data, name)`, and replaces the file of that id, or is added if there is none; it is called
|
/// `(id, data, name)`, and replaces the file of that id, or is added if there is none; `name` is
|
||||||
/// `name`, whatever its data says. The files to `remove` are deleted. The selection is left alone.
|
/// its name when its data has none. The files to `remove` are deleted. The selection is left alone.
|
||||||
pub fn sync_files<H: History>(
|
pub fn sync_files<H: History>(
|
||||||
engine: &mut Engine<H>,
|
engine: &mut Engine<H>,
|
||||||
files: &[(Uuid, &[u8], &str)],
|
files: &[(Uuid, &[u8], &str)],
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ use crate::{Apply, CommandError, FileId, Load, State};
|
|||||||
pub struct ExternalFile<'a> {
|
pub struct ExternalFile<'a> {
|
||||||
pub id: FileId,
|
pub id: FileId,
|
||||||
pub data: &'a [u8],
|
pub data: &'a [u8],
|
||||||
/// Name of the file outside (the name of the file on the disk, without its extension), which
|
/// Name of the file when the data has none (the name of the file on the disk, without its
|
||||||
/// is its name here whatever the data says: a file has a single name.
|
/// extension).
|
||||||
pub name: &'a str,
|
pub name: &'a str,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -16,8 +16,7 @@ pub struct ExternalFile<'a> {
|
|||||||
/// that are given replace the ones of the same id (which stay where they are in the order, and keep
|
/// that are given replace the ones of the same id (which stay where they are in the order, and keep
|
||||||
/// their id), or are added at the end if there are none; the ones to `remove` are deleted.
|
/// their id), or are added at the end if there are none; the ones to `remove` are deleted.
|
||||||
///
|
///
|
||||||
/// Unlike [`crate::LoadFiles`], the files are named as they are outside (see [`ExternalFile`]), and
|
/// Unlike [`crate::LoadFiles`], the selection is left alone (what it holds of a file that was
|
||||||
/// the selection is left alone (what it holds of a file that was
|
|
||||||
/// replaced or deleted is dropped by the engine afterwards, as with any edit).
|
/// replaced or deleted is dropped by the engine afterwards, as with any edit).
|
||||||
///
|
///
|
||||||
/// The files that cannot be read are skipped, and the files that are here stay as they are. It is an
|
/// The files that cannot be read are skipped, and the files that are here stay as they are. It is an
|
||||||
@@ -40,9 +39,6 @@ impl Apply for SyncFiles<'_> {
|
|||||||
match load.parse(state.categories) {
|
match load.parse(state.categories) {
|
||||||
Ok(mut file) => {
|
Ok(mut file) => {
|
||||||
file.id = external.id;
|
file.id = external.id;
|
||||||
if !external.name.trim().is_empty() {
|
|
||||||
file.info.name = external.name.to_owned();
|
|
||||||
}
|
|
||||||
if state.files.insert(external.id, Rc::new(file)).is_none() {
|
if state.files.insert(external.id, Rc::new(file)).is_none() {
|
||||||
state.order.0.push(external.id);
|
state.order.0.push(external.id);
|
||||||
}
|
}
|
||||||
@@ -107,8 +103,8 @@ mod tests {
|
|||||||
.apply(&mut fx.state())
|
.apply(&mut fx.state())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(fx.order.0, [first, second]);
|
assert_eq!(fx.order.0, [first, second]);
|
||||||
// named as it is outside, not as the data says
|
// named as the data says, the name outside is only for a file that has none
|
||||||
assert_eq!(fx.files[&first].info.name, "name");
|
assert_eq!(fx.files[&first].info.name, "from disk");
|
||||||
assert_eq!(fx.files[&first].id, first);
|
assert_eq!(fx.files[&first].id, first);
|
||||||
// the selection is not changed
|
// the selection is not changed
|
||||||
assert_eq!(fx.selected_files(), [second].into());
|
assert_eq!(fx.selected_files(), [second].into());
|
||||||
|
|||||||
+18
-19
@@ -14,7 +14,6 @@ use tauri::{AppHandle, Runtime, State};
|
|||||||
use tauri_plugin_dialog::{DialogExt, FilePath};
|
use tauri_plugin_dialog::{DialogExt, FilePath};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::commands::edit;
|
|
||||||
use crate::state::AppState;
|
use crate::state::AppState;
|
||||||
use crate::workspace::{Entry, Unsaved, WorkspaceState, free_path};
|
use crate::workspace::{Entry, Unsaved, WorkspaceState, free_path};
|
||||||
|
|
||||||
@@ -386,28 +385,28 @@ pub fn create_folder(
|
|||||||
state.core(move |core| core.workspace.create_folder(&parent, &name))
|
state.core(move |core| core.workspace.create_folder(&parent, &name))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Renames a file or a folder of the open folder. An open file is renamed like in its info (it is
|
/// Renames a file or a folder of the open folder, on the disk only: an open file keeps its name,
|
||||||
/// selected), which can be undone; the rest is renamed on the disk, and the open files that are in
|
/// and follows. Not undoable.
|
||||||
/// a folder that is renamed follow.
|
|
||||||
#[tauri::command(async)]
|
#[tauri::command(async)]
|
||||||
pub fn rename_path(
|
pub fn rename_path(state: State<AppState>, path: PathBuf, name: String) -> Result<(), String> {
|
||||||
state: State<AppState>,
|
|
||||||
path: PathBuf,
|
|
||||||
name: String,
|
|
||||||
) -> Result<api::Outcome, String> {
|
|
||||||
state.core(move |core| {
|
state.core(move |core| {
|
||||||
if let Some(id) = core.workspace.linked(&path) {
|
|
||||||
let selected = api::select(&mut core.engine, &[id], gpx_engine::SelectMode::Replace);
|
|
||||||
let desc = core
|
|
||||||
.engine
|
|
||||||
.file_structure(&FileId(id))
|
|
||||||
.and_then(|structure| structure.desc.clone())
|
|
||||||
.unwrap_or_default();
|
|
||||||
return Ok(selected.merge(edit::rename_selection(core, name, desc)));
|
|
||||||
}
|
|
||||||
core.workspace.rename_path(&path, &name)?;
|
core.workspace.rename_path(&path, &name)?;
|
||||||
core.commit(None);
|
core.commit(None);
|
||||||
Ok(api::Outcome::failed_none())
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Renames the file on the disk of an open file, next to where it is (see `Workspace::rename_file`).
|
||||||
|
/// Not undoable, the name of the file in the engine is not changed. Says whether it was renamed:
|
||||||
|
/// not for a file that is not on the disk.
|
||||||
|
#[tauri::command(async)]
|
||||||
|
pub fn rename_file(state: State<AppState>, file_id: Uuid, name: String) -> Result<bool, String> {
|
||||||
|
state.core(move |core| {
|
||||||
|
let renamed = core.workspace.rename_file(file_id, &name)?;
|
||||||
|
if renamed {
|
||||||
|
core.commit(None);
|
||||||
|
}
|
||||||
|
Ok(renamed)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,31 +1,17 @@
|
|||||||
//! Editing the metadata, the style, the structure and the waypoints of the files, and the tools:
|
//! Editing the metadata, the style, the structure and the waypoints of the files, and the tools:
|
||||||
//! reversing, cropping, reducing, timestamps, merging, cleaning...
|
//! reversing, cropping, reducing, timestamps, merging, cleaning...
|
||||||
|
|
||||||
use gpx_engine::{CleanType, FileId, MergeType, Selection, SplitType, api};
|
use gpx_engine::{CleanType, MergeType, SplitType, api};
|
||||||
use tauri::State;
|
use tauri::State;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::state::{AppState, Core};
|
use crate::state::AppState;
|
||||||
|
|
||||||
/// Names and describes the selected files or tracks. A file on the disk is renamed with it (and back
|
/// Names and describes the selected files or tracks. The files on the disk keep their name: see
|
||||||
/// when it is undone): its name is one that a file can have, and not the one of another file.
|
/// `disk::rename_file`.
|
||||||
#[tauri::command(async)]
|
#[tauri::command(async)]
|
||||||
pub fn metadata(state: State<AppState>, name: String, desc: String) -> api::Outcome {
|
pub fn metadata(state: State<AppState>, name: String, desc: String) -> api::Outcome {
|
||||||
state.core(move |core| rename_selection(core, name, desc))
|
state.run(move |e| api::metadata(e, &name, &desc))
|
||||||
}
|
|
||||||
|
|
||||||
/// See `metadata`.
|
|
||||||
pub fn rename_selection(core: &mut Core, name: String, desc: String) -> api::Outcome {
|
|
||||||
let mut name = name;
|
|
||||||
if let Selection::File { file_ids } = core.engine.selection() {
|
|
||||||
for FileId(id) in file_ids.clone() {
|
|
||||||
match core.workspace.name_for(id, &name) {
|
|
||||||
Ok(valid) => name = valid,
|
|
||||||
Err(error) => return api::Outcome::failed(error),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
core.edit(move |e| api::metadata(e, &name, &desc))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command(async)]
|
#[tauri::command(async)]
|
||||||
|
|||||||
@@ -115,10 +115,10 @@ pub fn move_elements(
|
|||||||
|
|
||||||
#[tauri::command(async)]
|
#[tauri::command(async)]
|
||||||
pub fn undo(state: State<AppState>) -> api::Outcome {
|
pub fn undo(state: State<AppState>) -> api::Outcome {
|
||||||
state.edit(api::undo)
|
state.run(api::undo)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command(async)]
|
#[tauri::command(async)]
|
||||||
pub fn redo(state: State<AppState>) -> api::Outcome {
|
pub fn redo(state: State<AppState>) -> api::Outcome {
|
||||||
state.edit(api::redo)
|
state.run(api::redo)
|
||||||
}
|
}
|
||||||
@@ -226,6 +226,7 @@ fn register_commands<R: tauri::Runtime>(builder: tauri::Builder<R>) -> tauri::Bu
|
|||||||
disk::list_folder,
|
disk::list_folder,
|
||||||
disk::create_folder,
|
disk::create_folder,
|
||||||
disk::rename_path,
|
disk::rename_path,
|
||||||
|
disk::rename_file,
|
||||||
disk::move_paths,
|
disk::move_paths,
|
||||||
disk::delete_paths,
|
disk::delete_paths,
|
||||||
// the system
|
// the system
|
||||||
|
|||||||
+7
-38
@@ -94,11 +94,8 @@ impl Core {
|
|||||||
self.commit(None);
|
self.commit(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Runs `f` on the engine, which can change: what changed is kept, and the files on the disk are
|
/// Runs `f` on the engine, which can change: what changed is kept.
|
||||||
/// renamed like the files of the engine were (an edit of the name, or its undo). Gives what
|
fn editing<T>(&mut self, f: impl FnOnce(&mut Engine) -> T) -> T {
|
||||||
/// could not be renamed.
|
|
||||||
fn editing<T>(&mut self, f: impl FnOnce(&mut Engine) -> T) -> (T, Vec<String>) {
|
|
||||||
let names = self.workspace.names(&self.engine);
|
|
||||||
let preview = self.workspace.preview;
|
let preview = self.workspace.preview;
|
||||||
let preview_before = preview.and_then(|id| signature(&self.engine, id));
|
let preview_before = preview.and_then(|id| signature(&self.engine, id));
|
||||||
let result = f(&mut self.engine);
|
let result = f(&mut self.engine);
|
||||||
@@ -107,32 +104,18 @@ impl Core {
|
|||||||
if let Some(id) = preview.filter(|_| kept) {
|
if let Some(id) = preview.filter(|_| kept) {
|
||||||
self.workspace.keep(&[id]);
|
self.workspace.keep(&[id]);
|
||||||
}
|
}
|
||||||
let (renamed, errors) = self.workspace.follow_names(&mut self.engine, &names);
|
|
||||||
// and it goes when something else is selected
|
// and it goes when something else is selected
|
||||||
let dropped = self.workspace.drop_preview(&mut self.engine);
|
let dropped = self.workspace.drop_preview(&mut self.engine);
|
||||||
if kept || renamed || dropped.is_some() {
|
if kept || dropped.is_some() {
|
||||||
self.commit(dropped.as_ref());
|
self.commit(dropped.as_ref());
|
||||||
} else {
|
} else {
|
||||||
self.persist();
|
self.persist();
|
||||||
}
|
}
|
||||||
(result, errors)
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Like `editing`, for an edit that can rename files: the outcome tells the first thing that
|
/// After files were written (`result`): what changed in the engine is kept and told, with where
|
||||||
/// could not be renamed (a key of the dictionaries of the frontend, which shows it).
|
/// the files are. Says whether they were written.
|
||||||
pub fn edit(&mut self, f: impl FnOnce(&mut Engine) -> api::Outcome) -> api::Outcome {
|
|
||||||
let (outcome, errors) = self.editing(f);
|
|
||||||
match errors.first() {
|
|
||||||
Some(error) if !outcome.has_error() => {
|
|
||||||
outcome.merge(api::Outcome::failed(error.clone()))
|
|
||||||
}
|
|
||||||
_ => outcome,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// After files were written (`result`): what changed in the engine (the files are called like
|
|
||||||
/// the files they were written to) is kept and told, with where the files are. Says whether
|
|
||||||
/// they were written.
|
|
||||||
pub fn wrote(&mut self, result: Result<api::Outcome, String>) -> Result<bool, String> {
|
pub fn wrote(&mut self, result: Result<api::Outcome, String>) -> Result<bool, String> {
|
||||||
self.commit(result.as_ref().ok().filter(|outcome| outcome.changed));
|
self.commit(result.as_ref().ok().filter(|outcome| outcome.changed));
|
||||||
result.map(|_| true)
|
result.map(|_| true)
|
||||||
@@ -325,21 +308,7 @@ impl AppState {
|
|||||||
|
|
||||||
/// Runs `f` on the engine, which can change: what changed is kept (see `Core::editing`).
|
/// Runs `f` on the engine, which can change: what changed is kept (see `Core::editing`).
|
||||||
pub fn run<T: Send + 'static>(&self, f: impl FnOnce(&mut Engine) -> T + Send + 'static) -> T {
|
pub fn run<T: Send + 'static>(&self, f: impl FnOnce(&mut Engine) -> T + Send + 'static) -> T {
|
||||||
self.core(move |core| {
|
self.core(move |core| core.editing(f))
|
||||||
let (result, errors) = core.editing(f);
|
|
||||||
for error in errors {
|
|
||||||
eprintln!("{error}");
|
|
||||||
}
|
|
||||||
result
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Like `run`, for an edit that can rename files: the outcome tells what could not be renamed.
|
|
||||||
pub fn edit(
|
|
||||||
&self,
|
|
||||||
f: impl FnOnce(&mut Engine) -> api::Outcome + Send + 'static,
|
|
||||||
) -> api::Outcome {
|
|
||||||
self.core(move |core| core.edit(f))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reads from the engine.
|
/// Reads from the engine.
|
||||||
|
|||||||
+65
-54
@@ -50,6 +50,20 @@ impl App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The error of a command that fails.
|
||||||
|
fn error(&self, cmd: &str, args: Value) -> Value {
|
||||||
|
let request = InvokeRequest {
|
||||||
|
cmd: cmd.into(),
|
||||||
|
callback: CallbackFn(0),
|
||||||
|
error: CallbackFn(1),
|
||||||
|
url: "tauri://localhost".parse().unwrap(),
|
||||||
|
body: InvokeBody::Json(args),
|
||||||
|
headers: Default::default(),
|
||||||
|
invoke_key: INVOKE_KEY.to_string(),
|
||||||
|
};
|
||||||
|
get_ipc_response(&self.0, request).expect_err(&format!("{cmd} did not fail"))
|
||||||
|
}
|
||||||
|
|
||||||
fn bytes(&self, cmd: &str, args: Value) -> Vec<u8> {
|
fn bytes(&self, cmd: &str, args: Value) -> Vec<u8> {
|
||||||
match self.request(cmd, InvokeBody::Json(args), Default::default()) {
|
match self.request(cmd, InvokeBody::Json(args), Default::default()) {
|
||||||
InvokeResponseBody::Raw(bytes) => bytes,
|
InvokeResponseBody::Raw(bytes) => bytes,
|
||||||
@@ -500,9 +514,9 @@ fn files_made_from_a_file_of_the_open_folder_are_written_next_to_it_at_once() {
|
|||||||
.values()
|
.values()
|
||||||
.map(|file| file.path.clone().unwrap())
|
.map(|file| file.path.clone().unwrap())
|
||||||
.collect();
|
.collect();
|
||||||
// the copy is in the subfolder, named after the file
|
// the copy is in the subfolder, named after its name (the one in the file, not of the file)
|
||||||
assert!(
|
assert!(
|
||||||
paths.iter().any(|path| path.ends_with("sub/track (2).gpx")),
|
paths.iter().any(|path| path.ends_with("sub/simple.gpx")),
|
||||||
"{paths:?}"
|
"{paths:?}"
|
||||||
);
|
);
|
||||||
// the new file went where the selected file is
|
// the new file went where the selected file is
|
||||||
@@ -536,8 +550,8 @@ fn the_parts_of_a_split_are_numbered_from_one() {
|
|||||||
.map(|entry| entry.file_name().to_string_lossy().into_owned())
|
.map(|entry| entry.file_name().to_string_lossy().into_owned())
|
||||||
.collect();
|
.collect();
|
||||||
names.sort();
|
names.sort();
|
||||||
// the file stays, its two parts are numbered from 1
|
// the file stays, its two parts are numbered from 1, under their name (the one in the file)
|
||||||
assert_eq!(names, ["track (1).gpx", "track (2).gpx", "track.gpx"]);
|
assert_eq!(names, ["simple (1).gpx", "simple (2).gpx", "track.gpx"]);
|
||||||
std::fs::remove_dir_all(folder).unwrap();
|
std::fs::remove_dir_all(folder).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -581,8 +595,18 @@ fn name_of(app: &App, id: &str) -> String {
|
|||||||
.to_string()
|
.to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The name of the file on the disk of an open file, without its extension.
|
||||||
|
fn file_stem_of(app: &App, id: &str) -> String {
|
||||||
|
let path = app.call("workspace_state", json!({}))["files"][id]["path"].clone();
|
||||||
|
std::path::Path::new(path.as_str().unwrap())
|
||||||
|
.file_stem()
|
||||||
|
.unwrap()
|
||||||
|
.to_string_lossy()
|
||||||
|
.into_owned()
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn a_file_is_called_like_its_file_and_renaming_it_renames_the_file() {
|
fn a_file_keeps_its_name_and_its_file_is_renamed_apart() {
|
||||||
let app = App::new();
|
let app = App::new();
|
||||||
let folder = folder_with(&["outside", "taken"]);
|
let folder = folder_with(&["outside", "taken"]);
|
||||||
let outcome = app.call(
|
let outcome = app.call(
|
||||||
@@ -590,76 +614,62 @@ fn a_file_is_called_like_its_file_and_renaming_it_renames_the_file() {
|
|||||||
json!({ "paths": [folder.join("outside.gpx")] }),
|
json!({ "paths": [folder.join("outside.gpx")] }),
|
||||||
);
|
);
|
||||||
let id = outcome["added"][0].as_str().unwrap().to_string();
|
let id = outcome["added"][0].as_str().unwrap().to_string();
|
||||||
|
let dirty = |app: &App| app.call("workspace_state", json!({}))["files"][&id]["dirty"].clone();
|
||||||
|
let inside = |name: &str| std::fs::read_to_string(folder.join(format!("{name}.gpx"))).unwrap();
|
||||||
|
|
||||||
// the name in the file is not the name of the file: the file wins, and nothing is unsaved
|
// the name in the file is its name, not the one of the file
|
||||||
assert_eq!(name_of(&app, &id), "outside");
|
assert_eq!(name_of(&app, &id), "Inside");
|
||||||
assert_eq!(
|
assert_eq!(file_stem_of(&app, &id), "outside");
|
||||||
app.call("workspace_state", json!({}))["files"][&id]["dirty"],
|
assert_eq!(dirty(&app), false);
|
||||||
false
|
|
||||||
);
|
|
||||||
|
|
||||||
// renaming renames the file on the disk
|
// renaming it is an edit of the file only, which is undone
|
||||||
app.call("select", json!({ "fileIds": [id], "mode": "replace" }));
|
app.call("select", json!({ "fileIds": [id], "mode": "replace" }));
|
||||||
let outcome = app.call("metadata", json!({ "name": "renamed", "desc": "" }));
|
let outcome = app.call("metadata", json!({ "name": "renamed", "desc": "" }));
|
||||||
assert!(outcome.get("error").is_none());
|
assert!(outcome.get("error").is_none());
|
||||||
assert!(folder.join("renamed.gpx").exists());
|
assert_eq!(name_of(&app, &id), "renamed");
|
||||||
assert!(!folder.join("outside.gpx").exists());
|
assert_eq!(file_stem_of(&app, &id), "outside");
|
||||||
let path = app.call("workspace_state", json!({}))["files"][&id]["path"].clone();
|
assert_eq!(dirty(&app), true);
|
||||||
assert!(path.as_str().unwrap().ends_with("renamed.gpx"));
|
|
||||||
// the file had nothing unsaved: it is written with its new name, and still has nothing unsaved
|
|
||||||
let dirty = |app: &App| app.call("workspace_state", json!({}))["files"][&id]["dirty"].clone();
|
|
||||||
let inside = |name: &str| std::fs::read_to_string(folder.join(format!("{name}.gpx"))).unwrap();
|
|
||||||
assert_eq!(dirty(&app), false);
|
|
||||||
assert!(inside("renamed").contains("<name>renamed</name>"));
|
|
||||||
|
|
||||||
// undone and redone, on the disk too, still with nothing unsaved
|
|
||||||
app.call("undo", json!({}));
|
app.call("undo", json!({}));
|
||||||
assert_eq!(name_of(&app, &id), "outside");
|
assert_eq!(name_of(&app, &id), "Inside");
|
||||||
assert!(folder.join("outside.gpx").exists() && !folder.join("renamed.gpx").exists());
|
|
||||||
assert_eq!(dirty(&app), false);
|
|
||||||
assert!(inside("outside").contains("<name>outside</name>"));
|
|
||||||
app.call("redo", json!({}));
|
|
||||||
assert_eq!(name_of(&app, &id), "renamed");
|
|
||||||
assert!(folder.join("renamed.gpx").exists() && !folder.join("outside.gpx").exists());
|
|
||||||
assert_eq!(dirty(&app), false);
|
assert_eq!(dirty(&app), false);
|
||||||
|
|
||||||
// the name of another file is not taken, nothing changes
|
// the file on the disk is renamed apart: not an edit, the file is as saved as it was
|
||||||
let outcome = app.call("metadata", json!({ "name": "taken", "desc": "" }));
|
let renamed = app.call("rename_file", json!({ "fileId": id, "name": "moved" }));
|
||||||
assert_eq!(outcome["error"], crate::workspace::NAME_TAKEN);
|
assert_eq!(renamed, true);
|
||||||
assert_eq!(name_of(&app, &id), "renamed");
|
assert!(folder.join("moved.gpx").exists() && !folder.join("outside.gpx").exists());
|
||||||
assert!(folder.join("renamed.gpx").exists());
|
assert_eq!(file_stem_of(&app, &id), "moved");
|
||||||
|
assert_eq!(name_of(&app, &id), "Inside");
|
||||||
|
assert_eq!(dirty(&app), false);
|
||||||
|
assert_eq!(app.call("can_undo", json!({})), false);
|
||||||
|
// saving writes to the new place, under the name of the file
|
||||||
|
app.call("metadata", json!({ "name": "saved", "desc": "" }));
|
||||||
|
app.call("save_files", json!({ "fileIds": [id] }));
|
||||||
|
assert!(inside("moved").contains("<name>saved</name>"));
|
||||||
|
assert_eq!(name_of(&app, &id), "saved");
|
||||||
|
|
||||||
|
// the name of another file is not taken
|
||||||
|
let error = app.error("rename_file", json!({ "fileId": id, "name": "taken" }));
|
||||||
|
assert_eq!(error, crate::workspace::NAME_TAKEN);
|
||||||
|
assert!(folder.join("moved.gpx").exists());
|
||||||
|
|
||||||
// a name is one that a file can have
|
// a name is one that a file can have
|
||||||
app.call("metadata", json!({ "name": "a/b", "desc": "" }));
|
app.call("rename_file", json!({ "fileId": id, "name": "a/b" }));
|
||||||
assert_eq!(name_of(&app, &id), "a_b");
|
|
||||||
assert!(folder.join("a_b.gpx").exists());
|
assert!(folder.join("a_b.gpx").exists());
|
||||||
|
|
||||||
// a file with edits is renamed, but not written: its edits are not saved for it
|
|
||||||
app.call("style", json!({ "color": "#ff0000" }));
|
|
||||||
assert_eq!(dirty(&app), true);
|
|
||||||
app.call("metadata", json!({ "name": "edited", "desc": "" }));
|
|
||||||
assert!(folder.join("edited.gpx").exists());
|
|
||||||
assert_eq!(dirty(&app), true);
|
|
||||||
assert!(!inside("edited").contains("ff0000"));
|
|
||||||
app.call("undo", json!({}));
|
|
||||||
app.call("undo", json!({}));
|
|
||||||
assert_eq!(name_of(&app, &id), "a_b");
|
|
||||||
assert_eq!(dirty(&app), false);
|
|
||||||
|
|
||||||
// only the case changes: the same file
|
// only the case changes: the same file
|
||||||
app.call("metadata", json!({ "name": "A_B", "desc": "" }));
|
app.call("rename_file", json!({ "fileId": id, "name": "A_B" }));
|
||||||
assert_eq!(name_of(&app, &id), "A_B");
|
|
||||||
let names: Vec<String> = std::fs::read_dir(&folder)
|
let names: Vec<String> = std::fs::read_dir(&folder)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.map(|entry| entry.unwrap().file_name().to_string_lossy().into_owned())
|
.map(|entry| entry.unwrap().file_name().to_string_lossy().into_owned())
|
||||||
.collect();
|
.collect();
|
||||||
assert!(names.contains(&"A_B.gpx".to_string()));
|
assert!(names.contains(&"A_B.gpx".to_string()));
|
||||||
assert_eq!(names.len(), 2);
|
assert_eq!(names.len(), 2);
|
||||||
|
assert_eq!(name_of(&app, &id), "saved");
|
||||||
std::fs::remove_dir_all(folder).unwrap();
|
std::fs::remove_dir_all(folder).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn a_new_file_written_under_another_name_takes_it() {
|
fn a_new_file_written_under_another_name_keeps_its_own() {
|
||||||
let app = App::new();
|
let app = App::new();
|
||||||
let folder = folder_with(&["route"]);
|
let folder = folder_with(&["route"]);
|
||||||
// a new file made in the folder is written there at once: route.gpx is taken
|
// a new file made in the folder is written there at once: route.gpx is taken
|
||||||
@@ -669,7 +679,8 @@ fn a_new_file_written_under_another_name_takes_it() {
|
|||||||
);
|
);
|
||||||
let id = created["added"][0].as_str().unwrap().to_string();
|
let id = created["added"][0].as_str().unwrap().to_string();
|
||||||
assert!(folder.join("route (2).gpx").exists());
|
assert!(folder.join("route (2).gpx").exists());
|
||||||
assert_eq!(name_of(&app, &id), "route (2)");
|
// the file keeps its name, whatever the name of the file it was written to
|
||||||
|
assert_eq!(name_of(&app, &id), "route");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
app.call("workspace_state", json!({}))["files"][&id]["dirty"],
|
app.call("workspace_state", json!({}))["files"][&id]["dirty"],
|
||||||
false
|
false
|
||||||
@@ -698,7 +709,7 @@ fn a_file_open_as_a_preview_is_replaced_until_it_is_kept_or_edited() {
|
|||||||
.as_array()
|
.as_array()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.iter()
|
.iter()
|
||||||
.map(|id| name_of(app, id.as_str().unwrap()))
|
.map(|id| file_stem_of(app, id.as_str().unwrap()))
|
||||||
.collect()
|
.collect()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+27
-86
@@ -695,8 +695,8 @@ impl Workspace {
|
|||||||
|
|
||||||
/// Writes files to the disk, replacing what is there: they are the same as their file from
|
/// Writes files to the disk, replacing what is there: they are the same as their file from
|
||||||
/// then on. A file that is not on the disk goes in its folder, or at its path in `paths`, or in
|
/// then on. A file that is not on the disk goes in its folder, or at its path in `paths`, or in
|
||||||
/// `folder`, under its name (changed if that is the one of something else), which the file then
|
/// `folder`, under its name (changed if that is the one of something else). Stops at the first
|
||||||
/// has (see `write_file`). Stops at the first error, the files that were written stay written.
|
/// error, the files that were written stay written.
|
||||||
pub fn save(
|
pub fn save(
|
||||||
&mut self,
|
&mut self,
|
||||||
engine: &mut Engine,
|
engine: &mut Engine,
|
||||||
@@ -725,8 +725,7 @@ impl Workspace {
|
|||||||
|
|
||||||
/// Writes the new files among `ids` in their folder, under their name. A name that is the one of
|
/// Writes the new files among `ids` in their folder, under their name. A name that is the one of
|
||||||
/// something else is changed (`name (2)`). Files that would be called the same, like the parts
|
/// 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)`). The files are called like the file
|
/// of a split, are numbered from 1 (`name (1)`, `name (2)`). The files keep their name.
|
||||||
/// they are written to.
|
|
||||||
pub fn save_new(&mut self, engine: &mut Engine, ids: &[Uuid]) -> Result<api::Outcome, String> {
|
pub fn save_new(&mut self, engine: &mut Engine, ids: &[Uuid]) -> Result<api::Outcome, String> {
|
||||||
let names: Vec<Option<(Uuid, PathBuf, String)>> = ids
|
let names: Vec<Option<(Uuid, PathBuf, String)>> = ids
|
||||||
.iter()
|
.iter()
|
||||||
@@ -760,30 +759,6 @@ impl Workspace {
|
|||||||
Ok(outcome)
|
Ok(outcome)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The names of the files that are on the disk, and whether they are the files on the disk, to
|
|
||||||
/// tell which ones were renamed by an edit (see `follow_names`).
|
|
||||||
pub fn names(&self, engine: &Engine) -> BTreeMap<Uuid, (String, bool)> {
|
|
||||||
self.links
|
|
||||||
.keys()
|
|
||||||
.filter_map(|id| {
|
|
||||||
let name = engine.file_structure(&FileId(*id))?.name.clone();
|
|
||||||
Some((*id, (name, !self.is_dirty(engine, *id))))
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The name that a file can be given. A file on the disk is called like its file: the name is
|
|
||||||
/// one that a file can have (see `file_name`), and not the one of another file next to it
|
|
||||||
/// (`NAME_TAKEN`). The other files get theirs when they are written.
|
|
||||||
pub fn name_for(&self, id: Uuid, name: &str) -> Result<String, String> {
|
|
||||||
if !self.links.contains_key(&id) {
|
|
||||||
return Ok(name.to_owned());
|
|
||||||
}
|
|
||||||
let name = file_name(name);
|
|
||||||
self.renamed_path(id, &name)?;
|
|
||||||
Ok(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Where a file on the disk goes when it is called `name`: next to where it is, with the same
|
/// Where a file on the disk goes when it is called `name`: next to where it is, with the same
|
||||||
/// extension. `None` if it is there already, an error if something else is.
|
/// extension. `None` if it is there already, an error if something else is.
|
||||||
fn renamed_path(&self, id: Uuid, name: &str) -> Result<Option<PathBuf>, String> {
|
fn renamed_path(&self, id: Uuid, name: &str) -> Result<Option<PathBuf>, String> {
|
||||||
@@ -813,49 +788,24 @@ impl Workspace {
|
|||||||
Ok(Some(path))
|
Ok(Some(path))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Renames the files on the disk whose name changed in the engine since it had `before` (see
|
/// Renames the file on the disk of an open file, next to where it is: the name is one that a
|
||||||
/// `names`): an edit of the name, or its undo, renames the file. A file that was the one on the
|
/// file can have (see `file_name`), and not the one of another file (`NAME_TAKEN`). It is not
|
||||||
/// disk is written too, so that it still is: the rename is not something to save. A file with
|
/// an edit, nothing is undone; the file has the same content, so it is as saved as it was.
|
||||||
/// edits is not, its edits are not saved for it. A file that cannot be renamed keeps its place,
|
/// Nothing happens to a file that is not on the disk. Says whether it was renamed.
|
||||||
/// with the reason in the errors. Says whether a file was renamed.
|
pub fn rename_file(&mut self, id: Uuid, name: &str) -> Result<bool, String> {
|
||||||
pub fn follow_names(
|
let Some(path) = self.renamed_path(id, name)? else {
|
||||||
&mut self,
|
return Ok(false);
|
||||||
engine: &mut Engine,
|
};
|
||||||
before: &BTreeMap<Uuid, (String, bool)>,
|
let link = self.links.get_mut(&id).expect("the file is on the disk");
|
||||||
) -> (bool, Vec<String>) {
|
// a file that is gone is written at its new place when it is saved
|
||||||
let mut renamed = false;
|
if !link.missing {
|
||||||
let mut errors = vec![];
|
fs::rename(&link.path, &path).map_err(|error| {
|
||||||
for (id, (old, clean)) in before {
|
|
||||||
let Some(structure) = engine.file_structure(&FileId(*id)) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
if structure.name == *old {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let path = match self.renamed_path(*id, &structure.name) {
|
|
||||||
Ok(Some(path)) => path,
|
|
||||||
Ok(None) => continue,
|
|
||||||
Err(error) => {
|
|
||||||
errors.push(error);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let link = self.links.get_mut(id).expect("the file is on the disk");
|
|
||||||
// a file that is gone is written at its new place when it is saved
|
|
||||||
if !link.missing
|
|
||||||
&& let Err(error) = fs::rename(&link.path, &path)
|
|
||||||
{
|
|
||||||
eprintln!("{} cannot be renamed: {error}", link.path.display());
|
eprintln!("{} cannot be renamed: {error}", link.path.display());
|
||||||
errors.push(RENAME_FAILED.to_owned());
|
RENAME_FAILED.to_owned()
|
||||||
continue;
|
})?;
|
||||||
}
|
|
||||||
link.path = path.clone();
|
|
||||||
renamed = true;
|
|
||||||
if *clean && let Err(error) = self.write_file(engine, *id, &path) {
|
|
||||||
errors.push(error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
(renamed, errors)
|
link.path = path;
|
||||||
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Writes a file to a path of the user's choice, which is the place of the file from then on.
|
/// Writes a file to a path of the user's choice, which is the place of the file from then on.
|
||||||
@@ -873,30 +823,21 @@ impl Workspace {
|
|||||||
self.write_file(engine, id, path)
|
self.write_file(engine, id, path)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Writes a file to `path`, which it is linked to from then on. It is called like the file it is
|
/// Writes a file to `path`, which it is linked to from then on. Says what changed in the engine
|
||||||
/// written to (a file has a single name), as if it always had been: that is not an edit to
|
/// (nothing: its name is its own, whatever the name of the file it is written to).
|
||||||
/// undo. Says what changed in the engine.
|
|
||||||
fn write_file(
|
fn write_file(
|
||||||
&mut self,
|
&mut self,
|
||||||
engine: &mut Engine,
|
engine: &mut Engine,
|
||||||
id: Uuid,
|
id: Uuid,
|
||||||
path: &Path,
|
path: &Path,
|
||||||
) -> Result<api::Outcome, String> {
|
) -> Result<api::Outcome, String> {
|
||||||
let Some(structure) = engine.file_structure(&FileId(id)) else {
|
let unchanged = api::Outcome::failed_none();
|
||||||
return Ok(api::Outcome::failed_none());
|
|
||||||
};
|
|
||||||
let name = stem(path);
|
|
||||||
let renamed = if structure.name != name {
|
|
||||||
api::rename_file(engine, id, &name)
|
|
||||||
} else {
|
|
||||||
api::Outcome::failed_none()
|
|
||||||
};
|
|
||||||
let Some(bytes) = api::export_file(engine, id, ExportOptions::ALL) else {
|
let Some(bytes) = api::export_file(engine, id, ExportOptions::ALL) else {
|
||||||
return Ok(renamed);
|
return Ok(unchanged);
|
||||||
};
|
};
|
||||||
write_atomically(path, &bytes).map_err(|error| format!("{}: {error}", path.display()))?;
|
write_atomically(path, &bytes).map_err(|error| format!("{}: {error}", path.display()))?;
|
||||||
let Some(signature) = signature(engine, id) else {
|
let Some(signature) = signature(engine, id) else {
|
||||||
return Ok(renamed);
|
return Ok(unchanged);
|
||||||
};
|
};
|
||||||
// the folder, if there is one, may watch what was just written: the hash tells it is known
|
// the folder, if there is one, may watch what was just written: the hash tells it is known
|
||||||
self.links.insert(
|
self.links.insert(
|
||||||
@@ -911,7 +852,7 @@ impl Workspace {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
self.dirs.remove(&id);
|
self.dirs.remove(&id);
|
||||||
Ok(renamed)
|
Ok(unchanged)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The folders and the GPX files that are in a folder of the open folder (or in it), the
|
/// The folders and the GPX files that are in a folder of the open folder (or in it), the
|
||||||
@@ -1026,8 +967,8 @@ impl Workspace {
|
|||||||
Ok(path)
|
Ok(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Renames a file or a folder of the tree that is not an open file (which is renamed in the
|
/// Renames a file or a folder of the tree. A file keeps its extension, and an open file keeps its
|
||||||
/// engine, see `follow_names`). A file keeps its extension. Gives the new path.
|
/// name (see `rename_file`). Gives the new path.
|
||||||
pub fn rename_path(&mut self, path: &Path, name: &str) -> Result<PathBuf, String> {
|
pub fn rename_path(&mut self, path: &Path, name: &str) -> Result<PathBuf, String> {
|
||||||
let path = self.tree_item(path)?;
|
let path = self.tree_item(path)?;
|
||||||
let name = file_name(name);
|
let name = file_name(name);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { Input } from '$lib/components/ui/input';
|
import { Input } from '$lib/components/ui/input';
|
||||||
import { Textarea } from '$lib/components/ui/textarea';
|
import { Textarea } from '$lib/components/ui/textarea';
|
||||||
import { Label } from '$lib/components/ui/label/index.js';
|
import { Label } from '$lib/components/ui/label/index.js';
|
||||||
|
import { Checkbox } from '$lib/components/ui/checkbox';
|
||||||
import * as Popover from '$lib/components/ui/popover';
|
import * as Popover from '$lib/components/ui/popover';
|
||||||
import { Save } from '@lucide/svelte';
|
import { Save } from '@lucide/svelte';
|
||||||
import { i18n } from '$lib/i18n/i18n.svelte';
|
import { i18n } from '$lib/i18n/i18n.svelte';
|
||||||
@@ -30,9 +31,48 @@
|
|||||||
node.type === 'file' ? (fileState.structure.desc ?? '') : (path?.desc ?? '')
|
node.type === 'file' ? (fileState.structure.desc ?? '') : (path?.desc ?? '')
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { workspace } = engine;
|
||||||
|
|
||||||
|
// the name of the file on the disk, without its extension (the desktop app), which is not the
|
||||||
|
// name of the file: it can be renamed with it, which is not undone
|
||||||
|
let fileName = $derived.by(() => {
|
||||||
|
if (node.type !== 'file') return undefined;
|
||||||
|
const path = $workspace?.files[node.fileId]?.path;
|
||||||
|
return path
|
||||||
|
?.split(/[\\/]/)
|
||||||
|
.pop()
|
||||||
|
?.replace(/\.[^.]*$/, '');
|
||||||
|
});
|
||||||
|
// asked only when the name is changed, and proposed then
|
||||||
|
let renamed = $derived(node.type === 'file' && name !== fileState.structure.name);
|
||||||
|
let renameFile = $state(true);
|
||||||
|
|
||||||
|
async function save() {
|
||||||
|
// closes the dialog now: `open` follows `editMetadata`, and the change of the file would
|
||||||
|
// open the dialog again before the effect below has reset it
|
||||||
|
editMetadata.current = false;
|
||||||
|
open = false;
|
||||||
|
// the file on the disk first: nothing is changed if it cannot be renamed (the name of
|
||||||
|
// another file, for instance)
|
||||||
|
if (
|
||||||
|
renamed &&
|
||||||
|
renameFile &&
|
||||||
|
fileName !== undefined &&
|
||||||
|
fileName !== name &&
|
||||||
|
node.type === 'file'
|
||||||
|
) {
|
||||||
|
if (!(await engine.renameFile(node.fileId, name))) {
|
||||||
|
showEngineError();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
engine.metadata(name, description);
|
||||||
|
}
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (!open) {
|
if (!open) {
|
||||||
editMetadata.current = false;
|
editMetadata.current = false;
|
||||||
|
renameFile = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@@ -44,22 +84,13 @@
|
|||||||
<Input bind:value={name} id="name" class="font-semibold h-8" />
|
<Input bind:value={name} id="name" class="font-semibold h-8" />
|
||||||
<Label for="description">{i18n._('menu.metadata.description')}</Label>
|
<Label for="description">{i18n._('menu.metadata.description')}</Label>
|
||||||
<Textarea bind:value={description} id="description" />
|
<Textarea bind:value={description} id="description" />
|
||||||
<Button
|
{#if fileName !== undefined && renamed}
|
||||||
variant="outline"
|
<div class="flex flex-row items-center gap-2">
|
||||||
onclick={() => {
|
<Checkbox id="rename-file" bind:checked={renameFile} class="scale-90" />
|
||||||
// closes the dialog now: `open` follows `editMetadata`, and the change of the file
|
<Label for="rename-file">{i18n._('menu.metadata.rename_file')}</Label>
|
||||||
// would open the dialog again before the effect below has reset it
|
</div>
|
||||||
editMetadata.current = false;
|
{/if}
|
||||||
open = false;
|
<Button variant="outline" onclick={save}>
|
||||||
// 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) => {
|
|
||||||
if (!changed) {
|
|
||||||
showEngineError();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Save size="16" />
|
<Save size="16" />
|
||||||
{i18n._('menu.metadata.save')}
|
{i18n._('menu.metadata.save')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -228,7 +228,8 @@ export async function createTauriBackend(): Promise<Backend> {
|
|||||||
openUrl: (url) => invoke('open_url', { url }),
|
openUrl: (url) => invoke('open_url', { url }),
|
||||||
newFile: (name, folder, route) => invoke<Outcome>('new_file', { name, folder, route }),
|
newFile: (name, folder, route) => invoke<Outcome>('new_file', { name, folder, route }),
|
||||||
createFolder: (parent, name) => invoke<string>('create_folder', { parent, name }),
|
createFolder: (parent, name) => invoke<string>('create_folder', { parent, name }),
|
||||||
renamePath: (path, name) => invoke<Outcome>('rename_path', { path, name }),
|
renamePath: (path, name) => invoke('rename_path', { path, name }),
|
||||||
|
renameFile: (fileId, name) => invoke<boolean>('rename_file', { fileId, name }),
|
||||||
movePaths: (paths, folder) => invoke('move_paths', { paths, folder }),
|
movePaths: (paths, folder) => invoke('move_paths', { paths, folder }),
|
||||||
deletePaths: (paths) => invoke<Outcome>('delete_paths', { paths }),
|
deletePaths: (paths) => invoke<Outcome>('delete_paths', { paths }),
|
||||||
revealPath: (path) => invoke('reveal_path', { path }),
|
revealPath: (path) => invoke('reveal_path', { path }),
|
||||||
|
|||||||
@@ -108,10 +108,15 @@ export interface DesktopFiles {
|
|||||||
/** Makes a folder in a folder of the open folder (numbered if the name is taken): its path. */
|
/** Makes a folder in a folder of the open folder (numbered if the name is taken): its path. */
|
||||||
createFolder(parent: string, name: string): Promise<string>;
|
createFolder(parent: string, name: string): Promise<string>;
|
||||||
/**
|
/**
|
||||||
* Renames a file or a folder of the open folder. An open file is renamed like in its info (it
|
* Renames a file or a folder of the open folder, on the disk only (an open file keeps its name).
|
||||||
* is selected), which can be undone.
|
* Not undoable.
|
||||||
*/
|
*/
|
||||||
renamePath(path: string, name: string): Promise<Outcome>;
|
renamePath(path: string, name: string): Promise<void>;
|
||||||
|
/**
|
||||||
|
* Renames the file on the disk of an open file, next to where it is; its name in the engine
|
||||||
|
* stays. Not undoable. False for a file that is not on the disk.
|
||||||
|
*/
|
||||||
|
renameFile(fileId: string, name: string): Promise<boolean>;
|
||||||
/** Moves files and folders of the open folder into a folder of it. */
|
/** Moves files and folders of the open folder into a folder of it. */
|
||||||
movePaths(paths: string[], folder: string): Promise<void>;
|
movePaths(paths: string[], folder: string): Promise<void>;
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -626,9 +626,21 @@ class Engine {
|
|||||||
.value;
|
.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Renames a file or a folder of the open folder. Resolves to whether it was done. */
|
/**
|
||||||
|
* Renames a file or a folder of the open folder, on the disk only: an open file keeps its name.
|
||||||
|
* Resolves to whether it was done.
|
||||||
|
*/
|
||||||
async renamePath(path: string, name: string): Promise<boolean> {
|
async renamePath(path: string, name: string): Promise<boolean> {
|
||||||
return this.desktopRun((b) => b.desktop!.renamePath(path, name), true);
|
return (await this.desktopCall((desktop) => desktop.renamePath(path, name), true)).done;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renames the file on the disk of an open file (the desktop app), which is not undoable: its
|
||||||
|
* name in the engine stays (see `metadata`). Resolves to whether it was done; nothing is done
|
||||||
|
* for a file that is not on the disk.
|
||||||
|
*/
|
||||||
|
async renameFile(fileId: string, name: string): Promise<boolean> {
|
||||||
|
return (await this.desktopCall((desktop) => desktop.renameFile(fileId, name), true)).done;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Moves files and folders of the open folder into a folder of it. */
|
/** Moves files and folders of the open folder into a folder of it. */
|
||||||
|
|||||||
@@ -100,6 +100,7 @@
|
|||||||
"name": "Name",
|
"name": "Name",
|
||||||
"description": "Description",
|
"description": "Description",
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
|
"rename_file": "Rename the file on disk",
|
||||||
"name_taken": "A file with this name already exists in the folder.",
|
"name_taken": "A file with this name already exists in the folder.",
|
||||||
"rename_failed": "The file could not be renamed."
|
"rename_failed": "The file could not be renamed."
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in new issue
Block a user