mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2026-11-01 20:19:39 +00:00
Migration to new Rust-based engine (#401)
* wasm first steps * parsing start * parsing continued * progress * progress * progress * progress * progress * progress * progress * progress * fixes * reorganize * change type * fix * progress * refactoring * progress * progress * progress * keep style info * progress * progress * remove (copy) in name * progress * merge methods into 1 * splice * progress * progress * progress * cleaning * progress * progress * progress * visibility * progress * progress * progress * progress * progress * fixes * progress * fix markers * fix * progress * progress * progress * progress * progress * progress * small fix * use engine info * fix popups * small fix * progress * progress * progress * clean tool * handle self closing elements * reduce data exchanges across boundary * copy on wasm side to avoid any data corruption * speed fix * routing * refactoring * reverse * reverse * crop * split * merge * progress * time tool * fix bounds for waypoints * reduce * fix reactivity * handle corrupt time data * export * progress * remove gpx js package * remove todo * update readme * cleaning * progress * fix link fields * remove unused fields * refactoring and tests * import and export of routes, ref #192 * per-chunk categories storage * cross-tab sync * outdated state * refactoring * reorder methods * refactoring * fix * improve flow * renaming * progress * progress * fix * progress * per file history * keep original flag * use history variant * fix * progress * file explorer * progress * fix * sticky folders * improved sticky folders * fix sortable * fix sortable * fix * fix tab size * no file delete on desktop * fix routing on other file * fix sorting * remember language * open links in browser * commit missing changes * avoid selecting on resize * clarify filename * explorer actions * fix folder styling * drop anywhere in folder * cleaning * fixes * remove focus-ring * full screen * small fix * consistent highlight color * improved preview * performance improvements * sparser data transfers * sparser data transfers * show popup once data is loaded * fix routing control size * fix * sparse desktop build * remove custom ign styles * use new endpoint for openhikingmap * strip native menu * reorder context menu items * route full support * route - track conversion * create route file action * route export, filter out anchors at high zoom level * clarify name handling * move hide & center to view * routes continued * update docs * fix power format, closes #378 * lint fixes * fix display of additional data with gaps, closes #361 * fix linting issues * fix import modules * fix multidrag * change base to resolve * fix type errors * fix layer typings * fix last type error * each keys * improve typings * deploy to cloudflare --------- Co-authored-by: vcoppe <vcoppe@hexaly.com>
This commit is contained in:
1 parent
6a0d434371
commit
8fe7d8d64d
484 files changed
+53949
-41152
No files matched your search
@@ -0,0 +1,454 @@
|
||||
//! The files of the engine and the file system: picking files and folders, saving, closing, and
|
||||
//! what the frontend shows of it (see `workspace`).
|
||||
//!
|
||||
//! The commands that can lose the edits of a file (closing, opening another folder) take what to do
|
||||
//! with the files that have some: the frontend asks the user, and the files that have edits stay
|
||||
//! open if it does not say.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use gpx_engine::{ExportOptions, FileId, api};
|
||||
use serde::Deserialize;
|
||||
use tauri::{AppHandle, Runtime, State};
|
||||
use tauri_plugin_dialog::{DialogExt, FilePath};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::state::AppState;
|
||||
use crate::workspace::{Entry, Unsaved, WorkspaceState, free_path};
|
||||
|
||||
fn paths(picked: Vec<FilePath>) -> Vec<PathBuf> {
|
||||
picked
|
||||
.into_iter()
|
||||
.filter_map(|path| path.into_path().ok())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Asks where to write a GPX file, proposing `file_name` in `folder`. `None` if the user cancelled.
|
||||
fn ask_save_path<R: Runtime>(
|
||||
app: &AppHandle<R>,
|
||||
file_name: &str,
|
||||
folder: Option<&Path>,
|
||||
) -> Option<PathBuf> {
|
||||
let mut dialog = app
|
||||
.dialog()
|
||||
.file()
|
||||
.add_filter("GPX", &["gpx"])
|
||||
.set_file_name(file_name);
|
||||
if let Some(folder) = folder {
|
||||
dialog = dialog.set_directory(folder);
|
||||
}
|
||||
dialog.blocking_save_file()?.into_path().ok()
|
||||
}
|
||||
|
||||
/// Asks for a folder, starting in `folder`. `None` if the user cancelled.
|
||||
fn ask_folder<R: Runtime>(app: &AppHandle<R>, folder: Option<&Path>) -> Option<PathBuf> {
|
||||
let mut dialog = app.dialog().file();
|
||||
if let Some(folder) = folder {
|
||||
dialog = dialog.set_directory(folder);
|
||||
}
|
||||
dialog.blocking_pick_folder()?.into_path().ok()
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
pub fn workspace_state(state: State<AppState>) -> WorkspaceState {
|
||||
state.core(|core| core.workspace_state())
|
||||
}
|
||||
|
||||
/// Opens the files that the user picks. Nothing happens if they pick none.
|
||||
#[tauri::command(async)]
|
||||
pub fn open_files<R: Runtime>(app: AppHandle<R>, state: State<AppState>) -> api::Outcome {
|
||||
let picked = app
|
||||
.dialog()
|
||||
.file()
|
||||
.add_filter("GPX", &["gpx"])
|
||||
.blocking_pick_files();
|
||||
let Some(picked) = picked else {
|
||||
return api::Outcome::failed_none();
|
||||
};
|
||||
open_paths(state, paths(picked), None)
|
||||
}
|
||||
|
||||
/// Opens files that are on the disk, like the ones of the tree of the open folder. They get selected.
|
||||
/// With `preview`, a single file is open as a preview (a single click in the tree, see
|
||||
/// `Workspace::open`); otherwise they are open for good, even the preview.
|
||||
#[tauri::command(async)]
|
||||
pub fn open_paths(
|
||||
state: State<AppState>,
|
||||
paths: Vec<PathBuf>,
|
||||
preview: Option<bool>,
|
||||
) -> api::Outcome {
|
||||
state.core(move |core| {
|
||||
let outcome = core
|
||||
.workspace
|
||||
.open(&mut core.engine, &paths, preview.unwrap_or_default());
|
||||
core.commit(None);
|
||||
outcome
|
||||
})
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// 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.
|
||||
///
|
||||
/// The files whose file on the disk was replaced are overwritten: the frontend asks before.
|
||||
fn write_unsaved<R: Runtime>(
|
||||
app: &AppHandle<R>,
|
||||
state: &AppState,
|
||||
ids: Vec<Uuid>,
|
||||
) -> Result<bool, String> {
|
||||
let (unlinked, start) = {
|
||||
let ids = ids.clone();
|
||||
state.core(move |core| {
|
||||
(
|
||||
core.workspace.unlinked(&core.engine, &ids),
|
||||
core.workspace.folder.clone(),
|
||||
)
|
||||
})
|
||||
};
|
||||
let mut chosen = BTreeMap::new();
|
||||
let mut folder = None;
|
||||
match unlinked.as_slice() {
|
||||
[] => {}
|
||||
[(id, name)] => match ask_save_path(app, &format!("{name}.gpx"), start.as_deref()) {
|
||||
Some(path) => {
|
||||
chosen.insert(*id, path);
|
||||
}
|
||||
None => return Ok(false),
|
||||
},
|
||||
_ => match ask_folder(app, start.as_deref()) {
|
||||
Some(path) => folder = Some(path),
|
||||
None => return Ok(false),
|
||||
},
|
||||
}
|
||||
state.core(move |core| {
|
||||
let result = core
|
||||
.workspace
|
||||
.save(&mut core.engine, &ids, &chosen, folder.as_deref());
|
||||
core.wrote(result)
|
||||
})
|
||||
}
|
||||
|
||||
/// Writes files to the disk, replacing what is there. The ones that are not on the disk yet go in
|
||||
/// their folder, or where the user says. Says whether they were written (`false` if the user
|
||||
/// cancelled).
|
||||
#[tauri::command(async)]
|
||||
pub fn save_files<R: Runtime>(
|
||||
app: AppHandle<R>,
|
||||
state: State<AppState>,
|
||||
file_ids: Vec<Uuid>,
|
||||
) -> Result<bool, String> {
|
||||
write_unsaved(&app, &state, file_ids)
|
||||
}
|
||||
|
||||
/// Writes a file where the user says, which is where the file is from then on. Says whether it was
|
||||
/// written (`false` if the user cancelled).
|
||||
#[tauri::command(async)]
|
||||
pub fn save_file_as<R: Runtime>(
|
||||
app: AppHandle<R>,
|
||||
state: State<AppState>,
|
||||
file_id: Uuid,
|
||||
) -> Result<bool, String> {
|
||||
let (name, current, folder) = state.core(move |core| {
|
||||
let name = core
|
||||
.engine
|
||||
.file_structure(&FileId(file_id))
|
||||
.map(|structure| structure.name.clone());
|
||||
let status = core.workspace_state();
|
||||
let file = status.files.get(&file_id.to_string());
|
||||
let current = file.and_then(|file| file.path.clone().map(PathBuf::from));
|
||||
let folder = file
|
||||
.and_then(|file| file.folder.clone().map(PathBuf::from))
|
||||
.or(status.folder.map(PathBuf::from));
|
||||
(name, current, folder)
|
||||
});
|
||||
let Some(name) = name else {
|
||||
return Ok(false);
|
||||
};
|
||||
// where the file is, or else where it would be written
|
||||
let folder = current
|
||||
.as_ref()
|
||||
.and_then(|path| path.parent().map(Path::to_path_buf))
|
||||
.or(folder);
|
||||
let Some(path) = ask_save_path(&app, &format!("{name}.gpx"), folder.as_deref()) else {
|
||||
return Ok(false);
|
||||
};
|
||||
state.core(move |core| {
|
||||
let result = core.workspace.save_as(&mut core.engine, file_id, &path);
|
||||
core.wrote(result)
|
||||
})
|
||||
}
|
||||
|
||||
/// Reads files again from the disk: the edits they have are lost (they are one undo step).
|
||||
#[tauri::command(async)]
|
||||
pub fn reload_files(state: State<AppState>, file_ids: Vec<Uuid>) -> Result<api::Outcome, String> {
|
||||
state.core(move |core| {
|
||||
let outcome = core.workspace.reload(&mut core.engine, &file_ids);
|
||||
core.commit(None);
|
||||
outcome
|
||||
})
|
||||
}
|
||||
|
||||
/// The files among `ids` that are not the ones on the disk, saved first if `unsaved` says so.
|
||||
/// Says whether to go on, which is not the case if the user cancelled saving them.
|
||||
fn settle<R: Runtime>(
|
||||
app: &AppHandle<R>,
|
||||
state: &AppState,
|
||||
ids: Vec<Uuid>,
|
||||
unsaved: Option<Unsaved>,
|
||||
) -> Result<bool, String> {
|
||||
if unsaved != Some(Unsaved::Save) {
|
||||
return Ok(true);
|
||||
}
|
||||
let unsaved_ids = state.core(move |core| core.workspace.unsaved(&core.engine, &ids));
|
||||
if unsaved_ids.is_empty() {
|
||||
return Ok(true);
|
||||
}
|
||||
write_unsaved(app, state, unsaved_ids)
|
||||
}
|
||||
|
||||
/// Opens the folder that the user picks, which is watched. Its files are not opened. The folder
|
||||
/// that was open is closed with its files: `unsaved` says what to do with the ones that have
|
||||
/// unsaved edits.
|
||||
#[tauri::command(async)]
|
||||
pub fn open_folder<R: Runtime>(
|
||||
app: AppHandle<R>,
|
||||
state: State<AppState>,
|
||||
unsaved: Option<Unsaved>,
|
||||
) -> Result<api::Outcome, String> {
|
||||
let Some(folder) = app
|
||||
.dialog()
|
||||
.file()
|
||||
.blocking_pick_folder()
|
||||
.and_then(|folder| folder.into_path().ok())
|
||||
else {
|
||||
return Ok(api::Outcome::failed_none());
|
||||
};
|
||||
let all = state.core(|core| core.workspace.all(&core.engine));
|
||||
if !settle(&app, &state, all, unsaved)? {
|
||||
return Ok(api::Outcome::failed_none());
|
||||
}
|
||||
state.core(move |core| {
|
||||
let discard = unsaved.is_some();
|
||||
let outcome = core
|
||||
.workspace
|
||||
.open_folder(&mut core.engine, &folder, discard);
|
||||
core.commit(None);
|
||||
Ok(outcome)
|
||||
})
|
||||
}
|
||||
|
||||
/// Closes the folder: it is not watched anymore, and the files are closed with it. `unsaved` says
|
||||
/// what to do with the ones that have unsaved edits (they stay open without it).
|
||||
#[tauri::command(async)]
|
||||
pub fn close_folder<R: Runtime>(
|
||||
app: AppHandle<R>,
|
||||
state: State<AppState>,
|
||||
unsaved: Option<Unsaved>,
|
||||
) -> Result<api::Outcome, String> {
|
||||
let all = state.core(|core| core.workspace.all(&core.engine));
|
||||
if !settle(&app, &state, all, unsaved)? {
|
||||
return Ok(api::Outcome::failed_none());
|
||||
}
|
||||
state.core(move |core| {
|
||||
let outcome = core
|
||||
.workspace
|
||||
.close_folder(&mut core.engine, unsaved.is_some());
|
||||
core.commit(None);
|
||||
Ok(outcome)
|
||||
})
|
||||
}
|
||||
|
||||
/// Closes files. `unsaved` says what to do with the ones that have unsaved edits: they are saved
|
||||
/// first, or the edits are lost (they stay open without it).
|
||||
#[tauri::command(async)]
|
||||
pub fn close_files<R: Runtime>(
|
||||
app: AppHandle<R>,
|
||||
state: State<AppState>,
|
||||
file_ids: Vec<Uuid>,
|
||||
unsaved: Option<Unsaved>,
|
||||
) -> Result<api::Outcome, String> {
|
||||
if !settle(&app, &state, file_ids.clone(), unsaved)? {
|
||||
return Ok(api::Outcome::failed_none());
|
||||
}
|
||||
state.core(move |core| {
|
||||
let outcome = core
|
||||
.workspace
|
||||
.close_files(&mut core.engine, &file_ids, unsaved.is_some());
|
||||
core.commit(None);
|
||||
Ok(outcome)
|
||||
})
|
||||
}
|
||||
|
||||
/// The folders and GPX files that are in a folder of the open folder, or in it (the root of the
|
||||
/// tree when no path is given).
|
||||
#[tauri::command(async)]
|
||||
pub fn list_folder(state: State<AppState>, path: Option<PathBuf>) -> Result<Vec<Entry>, String> {
|
||||
state.core(move |core| {
|
||||
let Some(path) = path.or_else(|| core.workspace.folder.clone()) else {
|
||||
return Ok(vec![]);
|
||||
};
|
||||
core.workspace.list_folder(&path)
|
||||
})
|
||||
}
|
||||
|
||||
/// What the files are exported with, see `ExportOptions`.
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ExportArguments {
|
||||
time: bool,
|
||||
hr: bool,
|
||||
cad: bool,
|
||||
atemp: bool,
|
||||
power: bool,
|
||||
osm: bool,
|
||||
}
|
||||
|
||||
impl From<ExportArguments> for ExportOptions {
|
||||
fn from(arguments: ExportArguments) -> Self {
|
||||
Self {
|
||||
time: arguments.time,
|
||||
hr: arguments.hr,
|
||||
cad: arguments.cad,
|
||||
atemp: arguments.atemp,
|
||||
power: arguments.power,
|
||||
osm: arguments.osm,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Writes copies of files where the user says: a file dialog for a single one, a folder for
|
||||
/// several. The files stay as they are. Says whether they were written (`false` if the user
|
||||
/// cancelled).
|
||||
#[tauri::command(async)]
|
||||
pub fn export_files<R: Runtime>(
|
||||
app: AppHandle<R>,
|
||||
state: State<AppState>,
|
||||
file_ids: Vec<Uuid>,
|
||||
options: ExportArguments,
|
||||
) -> Result<bool, String> {
|
||||
let options = ExportOptions::from(options);
|
||||
let files: Vec<(String, Vec<u8>)> = state.core(move |core| {
|
||||
file_ids
|
||||
.iter()
|
||||
.filter_map(|id| {
|
||||
let name = core.engine.file_structure(&FileId(*id))?.name.clone();
|
||||
Some((name, api::export_file(&core.engine, *id, options)?))
|
||||
})
|
||||
.collect()
|
||||
});
|
||||
let write = |path: PathBuf, bytes: &[u8]| {
|
||||
crate::store::write_atomically(&path, bytes)
|
||||
.map_err(|error| format!("{}: {error}", path.display()))
|
||||
};
|
||||
match files.as_slice() {
|
||||
[] => Ok(false),
|
||||
[(name, bytes)] => {
|
||||
let Some(path) = ask_save_path(&app, &format!("{name}.gpx"), None) else {
|
||||
return Ok(false);
|
||||
};
|
||||
write(path, bytes).map(|()| true)
|
||||
}
|
||||
_ => {
|
||||
let Some(folder) = ask_folder(&app, None) else {
|
||||
return Ok(false);
|
||||
};
|
||||
let mut written: Vec<PathBuf> = vec![];
|
||||
for (name, bytes) in &files {
|
||||
let path = free_path(&folder, name, |path| written.iter().any(|w| w == path));
|
||||
write(path.clone(), bytes)?;
|
||||
written.push(path);
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Makes a folder in a folder of the open folder, called `name` (numbered if that is taken). Gives
|
||||
/// its path.
|
||||
#[tauri::command(async)]
|
||||
pub fn create_folder(
|
||||
state: State<AppState>,
|
||||
parent: PathBuf,
|
||||
name: String,
|
||||
) -> Result<PathBuf, String> {
|
||||
state.core(move |core| core.workspace.create_folder(&parent, &name))
|
||||
}
|
||||
|
||||
/// Renames a file or a folder of the open folder, on the disk only: an open file keeps its name,
|
||||
/// and follows. Not undoable.
|
||||
#[tauri::command(async)]
|
||||
pub fn rename_path(state: State<AppState>, path: PathBuf, name: String) -> Result<(), String> {
|
||||
state.core(move |core| {
|
||||
core.workspace.rename_path(&path, &name)?;
|
||||
core.commit(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)
|
||||
})
|
||||
}
|
||||
|
||||
/// Moves files and folders of the open folder into a folder of it. The open files follow.
|
||||
#[tauri::command(async)]
|
||||
pub fn move_paths(
|
||||
state: State<AppState>,
|
||||
paths: Vec<PathBuf>,
|
||||
folder: PathBuf,
|
||||
) -> Result<(), String> {
|
||||
state.core(move |core| {
|
||||
let result = core.workspace.move_paths(&paths, &folder);
|
||||
core.commit(None);
|
||||
result
|
||||
})
|
||||
}
|
||||
|
||||
/// Moves files and folders of the open folder to the trash. The open files that were among them
|
||||
/// are closed, unless they have edits: they stay open, as gone from the disk.
|
||||
#[tauri::command(async)]
|
||||
pub fn delete_paths(state: State<AppState>, paths: Vec<PathBuf>) -> Result<api::Outcome, String> {
|
||||
state.core(move |core| {
|
||||
let outcome = core
|
||||
.workspace
|
||||
.delete_paths(&mut core.engine, &paths, to_trash)?;
|
||||
core.commit(None);
|
||||
Ok(outcome)
|
||||
})
|
||||
}
|
||||
|
||||
/// Moves files and folders to the trash of the system.
|
||||
fn to_trash(paths: &[PathBuf]) -> Result<(), String> {
|
||||
#[allow(unused_mut)]
|
||||
let mut trash = trash::TrashContext::default();
|
||||
// without asking the Finder, which needs a permission
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
use trash::macos::{DeleteMethod, TrashContextExtMacos};
|
||||
trash.set_delete_method(DeleteMethod::NsFileManager);
|
||||
}
|
||||
trash.delete_all(paths).map_err(|error| {
|
||||
eprintln!("the files cannot be deleted: {error}");
|
||||
crate::workspace::DELETE_FAILED.to_owned()
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
//! Editing the metadata, the style, the structure and the waypoints of the files, and the tools:
|
||||
//! reversing, cropping, reducing, timestamps, merging, cleaning...
|
||||
|
||||
use gpx_engine::{CleanType, MergeType, SplitType, api};
|
||||
use tauri::State;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::state::AppState;
|
||||
|
||||
/// Names and describes the selected files or tracks. The files on the disk keep their name: see
|
||||
/// `disk::rename_file`.
|
||||
#[tauri::command(async)]
|
||||
pub fn metadata(state: State<AppState>, name: String, desc: String) -> api::Outcome {
|
||||
state.run(move |e| api::metadata(e, &name, &desc))
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
pub fn style(
|
||||
state: State<AppState>,
|
||||
color: Option<String>,
|
||||
opacity: Option<f64>,
|
||||
width: Option<f64>,
|
||||
) -> api::Outcome {
|
||||
state.run(move |e| api::style(e, color.as_deref(), opacity, width))
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
pub fn convert(state: State<AppState>) -> api::Outcome {
|
||||
state.run(api::convert)
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
pub fn new_route(state: State<AppState>) -> api::Outcome {
|
||||
state.run(api::new_route)
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
pub fn new_track(state: State<AppState>) -> api::Outcome {
|
||||
state.run(api::new_track)
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
pub fn new_track_segment(state: State<AppState>) -> api::Outcome {
|
||||
state.run(api::new_track_segment)
|
||||
}
|
||||
|
||||
// Waypoints
|
||||
|
||||
#[tauri::command(async)]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new_waypoint(
|
||||
state: State<AppState>,
|
||||
lng: f64,
|
||||
lat: f64,
|
||||
ele: f64,
|
||||
name: String,
|
||||
desc: String,
|
||||
icon: String,
|
||||
link: String,
|
||||
) -> api::Outcome {
|
||||
state.run(move |e| api::new_waypoint(e, lng, lat, ele, &name, &desc, &icon, &link))
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
pub fn move_waypoint(
|
||||
state: State<AppState>,
|
||||
file_id: Uuid,
|
||||
waypoint_id: Uuid,
|
||||
lng: f64,
|
||||
lat: f64,
|
||||
ele: f64,
|
||||
) -> api::Outcome {
|
||||
state.run(move |e| api::move_waypoint(e, file_id, waypoint_id, lng, lat, ele))
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
pub fn delete_waypoint(state: State<AppState>, file_id: Uuid, waypoint_id: Uuid) -> api::Outcome {
|
||||
state.run(move |e| api::delete_waypoint(e, file_id, waypoint_id))
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn update_waypoint(
|
||||
state: State<AppState>,
|
||||
file_id: Uuid,
|
||||
waypoint_id: Uuid,
|
||||
lng: f64,
|
||||
lat: f64,
|
||||
ele: f64,
|
||||
name: String,
|
||||
desc: String,
|
||||
icon: String,
|
||||
link: String,
|
||||
) -> api::Outcome {
|
||||
state.run(move |e| {
|
||||
api::update_waypoint(
|
||||
e,
|
||||
file_id,
|
||||
waypoint_id,
|
||||
lng,
|
||||
lat,
|
||||
ele,
|
||||
&name,
|
||||
&desc,
|
||||
&icon,
|
||||
&link,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
// Tools
|
||||
|
||||
#[tauri::command(async)]
|
||||
pub fn reverse(state: State<AppState>) -> api::Outcome {
|
||||
state.run(api::reverse)
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
pub fn round_trip(state: State<AppState>) -> api::Outcome {
|
||||
state.run(api::round_trip)
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
pub fn crop(state: State<AppState>, start: u32, end: u32) -> api::Outcome {
|
||||
state.run(move |e| api::crop(e, start, end))
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
pub fn reduce(state: State<AppState>, tolerance: f64) -> api::Outcome {
|
||||
state.run(move |e| api::reduce(e, tolerance))
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
pub fn change_timestamps(
|
||||
state: State<AppState>,
|
||||
start_time: f64,
|
||||
speed: f64,
|
||||
ratio: f64,
|
||||
) -> api::Outcome {
|
||||
state.run(move |e| api::change_timestamps(e, start_time, speed, ratio))
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
pub fn create_artificial_timestamps(
|
||||
state: State<AppState>,
|
||||
start_time: f64,
|
||||
total_time: f64,
|
||||
) -> api::Outcome {
|
||||
state.run(move |e| api::create_artificial_timestamps(e, start_time, total_time))
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
pub fn merge(state: State<AppState>, merge_type: MergeType, remove_gaps: bool) -> api::Outcome {
|
||||
state.core(move |core| core.creating(None, false, |e| api::merge(e, merge_type, remove_gaps)))
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
pub fn extract(state: State<AppState>) -> api::Outcome {
|
||||
state.core(|core| core.creating(None, false, api::extract))
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
pub fn elevation(state: State<AppState>, ele: Vec<f64>) -> api::Outcome {
|
||||
state.run(move |e| api::elevation(e, &ele))
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn clean(
|
||||
state: State<AppState>,
|
||||
west: f64,
|
||||
south: f64,
|
||||
east: f64,
|
||||
north: f64,
|
||||
clean_type: CleanType,
|
||||
trkpt: bool,
|
||||
wpt: bool,
|
||||
) -> api::Outcome {
|
||||
state.run(move |e| api::clean(e, west, south, east, north, clean_type, trkpt, wpt))
|
||||
}
|
||||
|
||||
// Routing
|
||||
|
||||
#[tauri::command(async)]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn route(
|
||||
state: State<AppState>,
|
||||
revision: u32,
|
||||
start: u32,
|
||||
end: u32,
|
||||
lng: Vec<f64>,
|
||||
lat: Vec<f64>,
|
||||
ele: Vec<f64>,
|
||||
attributes: api::RouteAttributes,
|
||||
anchors: Vec<u32>,
|
||||
) -> api::Outcome {
|
||||
state.run(move |e| {
|
||||
api::route(
|
||||
e,
|
||||
revision,
|
||||
start,
|
||||
end,
|
||||
&lng,
|
||||
&lat,
|
||||
&ele,
|
||||
&attributes,
|
||||
&anchors,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
pub fn insert_anchor(state: State<AppState>, revision: u32, lng: f64, lat: f64) -> api::Outcome {
|
||||
state.run(move |e| api::insert_anchor(e, revision, lng, lat))
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
pub fn change_loop_start(state: State<AppState>, revision: u32, index: u32) -> api::Outcome {
|
||||
state.run(move |e| api::change_loop_start(e, revision, index))
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
pub fn split(
|
||||
state: State<AppState>,
|
||||
revision: u32,
|
||||
at: u32,
|
||||
split_type: SplitType,
|
||||
) -> api::Outcome {
|
||||
state.core(move |core| core.creating(None, false, |e| api::split(e, revision, at, split_type)))
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
//! Creating, loading, deleting, moving, copying and pasting files and their elements, and undoing.
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use gpx_engine::api;
|
||||
use tauri::State;
|
||||
use tauri::ipc::{InvokeBody, Request};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::state::AppState;
|
||||
|
||||
#[tauri::command(async)]
|
||||
pub fn new_file(
|
||||
state: State<AppState>,
|
||||
name: String,
|
||||
lng: Option<f64>,
|
||||
lat: Option<f64>,
|
||||
ele: Option<f64>,
|
||||
route: Option<bool>,
|
||||
folder: Option<PathBuf>,
|
||||
) -> api::Outcome {
|
||||
let route = route.unwrap_or_default();
|
||||
// in `folder` if it is given, otherwise like the selected file, otherwise in the open folder
|
||||
state.core(move |core| {
|
||||
core.creating(folder, true, |e| {
|
||||
api::new_file(e, &name, lng, lat, ele, route)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// The files are in the body, one after the other, the headers `lengths` and `names` (JSON,
|
||||
/// percent-encoded) telling the number of bytes and the name of each one.
|
||||
#[tauri::command(async)]
|
||||
pub fn load_files(state: State<AppState>, request: Request<'_>) -> api::Outcome {
|
||||
let InvokeBody::Raw(data) = request.body() else {
|
||||
return api::Outcome::failed("the files are missing");
|
||||
};
|
||||
let header = |key: &str| {
|
||||
request
|
||||
.headers()
|
||||
.get(key)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.and_then(percent_decode)
|
||||
};
|
||||
let (Some(lengths), Some(names)) = (
|
||||
header("lengths").and_then(|json| serde_json::from_str::<Vec<usize>>(&json).ok()),
|
||||
header("names").and_then(|json| serde_json::from_str::<Vec<String>>(&json).ok()),
|
||||
) else {
|
||||
return api::Outcome::failed("the lengths or the names of the files are not valid");
|
||||
};
|
||||
let data = data.clone();
|
||||
state.run(move |e| api::load_files(e, &data, &lengths, &names))
|
||||
}
|
||||
|
||||
fn percent_decode(value: &str) -> Option<String> {
|
||||
let bytes = value.as_bytes();
|
||||
let mut decoded = Vec::with_capacity(bytes.len());
|
||||
let mut i = 0;
|
||||
while i < bytes.len() {
|
||||
if bytes[i] == b'%' {
|
||||
let hex = value.get(i + 1..i + 3)?;
|
||||
decoded.push(u8::from_str_radix(hex, 16).ok()?);
|
||||
i += 3;
|
||||
} else {
|
||||
decoded.push(bytes[i]);
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
String::from_utf8(decoded).ok()
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
pub fn duplicate(state: State<AppState>) -> api::Outcome {
|
||||
state.core(|core| core.creating(None, false, api::duplicate))
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
pub fn delete(state: State<AppState>, whole_files: bool) -> api::Outcome {
|
||||
state.run(move |e| api::delete(e, whole_files))
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
pub fn delete_all(state: State<AppState>) -> api::Outcome {
|
||||
state.run(api::delete_all)
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
pub fn reorder(state: State<AppState>, file_ids: Vec<Uuid>, index: usize) -> api::Outcome {
|
||||
state.run(move |e| api::reorder(e, &file_ids, index))
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
pub fn copy(state: State<AppState>) -> api::Outcome {
|
||||
state.run(api::copy)
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
pub fn cut(state: State<AppState>) -> api::Outcome {
|
||||
state.run(api::cut)
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
pub fn paste(state: State<AppState>) -> api::Outcome {
|
||||
state.run(api::paste)
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
pub fn move_elements(
|
||||
state: State<AppState>,
|
||||
what: api::Selection,
|
||||
to: api::MoveTarget,
|
||||
) -> api::Outcome {
|
||||
state.run(move |e| api::move_elements(e, what, to))
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
pub fn undo(state: State<AppState>) -> api::Outcome {
|
||||
state.run(api::undo)
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
pub fn redo(state: State<AppState>) -> api::Outcome {
|
||||
state.run(api::redo)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
//! The functions of the frontend's interface to the engine, as Tauri commands. They are the
|
||||
//! counterparts of the functions that `gpx-rs/wasm` exports, and cross the same data: see
|
||||
//! `website/src/lib/engine/backend`.
|
||||
//!
|
||||
//! The commands that edit the files or change the selection return an `Outcome`.
|
||||
//! They are `async` (so they run off the main thread), the frontend sends them one after the other.
|
||||
|
||||
pub mod disk;
|
||||
pub mod edit;
|
||||
pub mod files;
|
||||
pub mod queries;
|
||||
pub mod selection;
|
||||
pub mod statistics;
|
||||
pub mod storage;
|
||||
pub mod system;
|
||||
@@ -0,0 +1,112 @@
|
||||
//! The files, their structure and statistics, the selection, the clipboard, and the coordinates of
|
||||
//! the trackpoints and waypoints (flat `[lng, lat, ...]` little-endian `f64` arrays).
|
||||
|
||||
use gpx_engine::{ExportOptions, api};
|
||||
use tauri::State;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::bytes::*;
|
||||
use crate::state::AppState;
|
||||
|
||||
#[tauri::command(async)]
|
||||
pub fn file_order(state: State<AppState>) -> Vec<String> {
|
||||
state.read(api::file_order)
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
pub fn file_structure(state: State<AppState>, file_id: Uuid) -> Option<api::FileStructure> {
|
||||
state.read(move |e| api::file_structure(e, file_id))
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
pub fn file_statistics(state: State<AppState>, file_id: Uuid) -> Option<api::GlobalStatistics> {
|
||||
state.read(move |e| api::file_statistics(e, file_id))
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
pub fn selection(state: State<AppState>) -> api::Selection {
|
||||
state.read(api::selection)
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
pub fn clipboard(state: State<AppState>) -> Option<api::Clipboard> {
|
||||
state.read(api::clipboard)
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
pub fn can_undo(state: State<AppState>) -> bool {
|
||||
state.read(api::can_undo)
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
pub fn can_redo(state: State<AppState>) -> bool {
|
||||
state.read(api::can_redo)
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
pub fn can_paste(state: State<AppState>) -> bool {
|
||||
state.read(api::can_paste)
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
pub fn waypoint(
|
||||
state: State<AppState>,
|
||||
file_id: Uuid,
|
||||
waypoint_id: Uuid,
|
||||
) -> Option<api::WaypointDetails> {
|
||||
state.read(move |e| api::waypoint(e, file_id, waypoint_id))
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
pub fn trackpoint(
|
||||
state: State<AppState>,
|
||||
file_id: Uuid,
|
||||
segment_id: Uuid,
|
||||
index: usize,
|
||||
) -> Option<api::TrackpointDetails> {
|
||||
state.read(move |e| api::trackpoint(e, file_id, segment_id, index))
|
||||
}
|
||||
|
||||
/// A file as GPX (UTF-8 bytes), the first byte telling whether the file exists.
|
||||
#[tauri::command(async)]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn export_file(
|
||||
state: State<AppState>,
|
||||
file_id: Uuid,
|
||||
time: bool,
|
||||
hr: bool,
|
||||
cad: bool,
|
||||
atemp: bool,
|
||||
power: bool,
|
||||
osm: bool,
|
||||
) -> Bytes {
|
||||
let options = ExportOptions {
|
||||
time,
|
||||
hr,
|
||||
cad,
|
||||
atemp,
|
||||
power,
|
||||
osm,
|
||||
};
|
||||
optional(state.read(move |e| api::export_file(e, file_id, options)))
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
pub fn exportable_data(state: State<AppState>, file_ids: Vec<Uuid>) -> u8 {
|
||||
state.read(move |e| api::exportable_data(e, &file_ids))
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
pub fn reduction_distances(state: State<AppState>) -> Bytes {
|
||||
f64s(&state.read(api::reduction_distances))
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
pub fn segment_coordinates(state: State<AppState>, segment_id: Uuid) -> Bytes {
|
||||
f64s(&state.read(move |e| api::segment_coordinates(e, segment_id).to_vec()))
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
pub fn waypoint_coordinates(state: State<AppState>, file_id: Uuid) -> Bytes {
|
||||
f64s(&state.read(move |e| api::waypoint_coordinates(e, file_id).to_vec()))
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
//! Changing what is selected.
|
||||
|
||||
use gpx_engine::{SelectMode, api};
|
||||
use tauri::State;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::state::AppState;
|
||||
|
||||
#[tauri::command(async)]
|
||||
pub fn select(state: State<AppState>, file_ids: Vec<Uuid>, mode: SelectMode) -> api::Outcome {
|
||||
state.run(move |e| api::select(e, &file_ids, mode))
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
pub fn select_all(state: State<AppState>) -> api::Outcome {
|
||||
state.run(api::select_all)
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
pub fn arrow_select(state: State<AppState>, down: bool, add: bool) -> api::Outcome {
|
||||
state.run(move |e| api::arrow_select(e, down, add))
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
pub fn select_routes(
|
||||
state: State<AppState>,
|
||||
file_id: Uuid,
|
||||
route_ids: Vec<Uuid>,
|
||||
mode: SelectMode,
|
||||
) -> api::Outcome {
|
||||
state.run(move |e| api::select_routes(e, file_id, &route_ids, mode))
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
pub fn select_tracks(
|
||||
state: State<AppState>,
|
||||
file_id: Uuid,
|
||||
track_ids: Vec<Uuid>,
|
||||
mode: SelectMode,
|
||||
) -> api::Outcome {
|
||||
state.run(move |e| api::select_tracks(e, file_id, &track_ids, mode))
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
pub fn select_segments(
|
||||
state: State<AppState>,
|
||||
file_id: Uuid,
|
||||
track_id: Uuid,
|
||||
segment_ids: Vec<Uuid>,
|
||||
mode: SelectMode,
|
||||
) -> api::Outcome {
|
||||
state.run(move |e| api::select_segments(e, file_id, track_id, &segment_ids, mode))
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
pub fn select_waypoint_group(
|
||||
state: State<AppState>,
|
||||
file_id: Uuid,
|
||||
mode: SelectMode,
|
||||
) -> api::Outcome {
|
||||
state.run(move |e| api::select_waypoint_group(e, file_id, mode))
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
pub fn select_waypoints(
|
||||
state: State<AppState>,
|
||||
file_id: Uuid,
|
||||
waypoint_ids: Vec<Uuid>,
|
||||
mode: SelectMode,
|
||||
) -> api::Outcome {
|
||||
state.run(move |e| api::select_waypoints(e, file_id, &waypoint_ids, mode))
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
//! The statistics of the selection and the anchors of the routing tool, as little-endian arrays
|
||||
//! (one per call, copies of the buffers of the engine). Optional ones start with a byte telling
|
||||
//! whether they are there.
|
||||
|
||||
use gpx_engine::api;
|
||||
use tauri::State;
|
||||
|
||||
use crate::bytes::*;
|
||||
use crate::state::AppState;
|
||||
|
||||
macro_rules! f64_getters {
|
||||
($($name:ident),*) => {$(
|
||||
#[tauri::command(async)]
|
||||
pub fn $name(state: State<AppState>) -> Bytes {
|
||||
f64s(&state.read(|e| api::$name(e).to_vec()))
|
||||
}
|
||||
)*};
|
||||
}
|
||||
|
||||
f64_getters!(total_distance, slope_segment_slopes, lng, lat, ele);
|
||||
|
||||
macro_rules! optional_f64_getters {
|
||||
($($name:ident),*) => {$(
|
||||
#[tauri::command(async)]
|
||||
pub fn $name(state: State<AppState>) -> Bytes {
|
||||
optional(state.read(|e| api::$name(e).map(f64_bytes)))
|
||||
}
|
||||
)*};
|
||||
}
|
||||
|
||||
optional_f64_getters!(speed, hr, cad, atemp, power);
|
||||
|
||||
/// The index of the first trackpoint of each smoothed elevation segment of the selection.
|
||||
#[tauri::command(async)]
|
||||
pub fn slope_segment_starts(state: State<AppState>) -> Bytes {
|
||||
u32s(&state.read(|e| api::slope_segment_starts(e).to_vec()))
|
||||
}
|
||||
|
||||
/// What the buffers do not hold for each trackpoint of the selection, for the one at `index`.
|
||||
#[tauri::command(async)]
|
||||
pub fn selection_point(state: State<AppState>, index: usize) -> Option<api::SelectionPoint> {
|
||||
state.read(move |e| api::selection_point(e, index))
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
pub fn selection_statistics(state: State<AppState>) -> api::GlobalStatistics {
|
||||
state.read(api::selection_statistics)
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
pub fn slice_statistics(
|
||||
state: State<AppState>,
|
||||
start: usize,
|
||||
end: usize,
|
||||
) -> Option<api::GlobalStatistics> {
|
||||
state.read(move |e| api::slice_statistics(e, start, end))
|
||||
}
|
||||
|
||||
macro_rules! category_getters {
|
||||
($starts:ident, $values:ident, $names:ident) => {
|
||||
#[tauri::command(async)]
|
||||
pub fn $starts(state: State<AppState>) -> Bytes {
|
||||
u32s(&state.read(|e| api::$starts(e).to_vec()))
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
pub fn $values(state: State<AppState>) -> Bytes {
|
||||
u8s(&state.read(|e| api::$values(e).to_vec()))
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
pub fn $names(state: State<AppState>) -> Vec<String> {
|
||||
state.read(|e| api::$names(e).to_vec())
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
category_getters!(surface_starts, surface_values, surfaces);
|
||||
category_getters!(highway_starts, highway_values, highways);
|
||||
category_getters!(sac_scale_starts, sac_scale_values, sac_scales);
|
||||
category_getters!(mtb_scale_starts, mtb_scale_values, mtb_scales);
|
||||
|
||||
#[tauri::command(async)]
|
||||
pub fn anchor_indices(state: State<AppState>) -> Bytes {
|
||||
u32s(&state.read(|e| api::anchor_indices(e).to_vec()))
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
pub fn anchor_zooms(state: State<AppState>) -> Bytes {
|
||||
u8s(&state.read(|e| api::anchor_zooms(e).to_vec()))
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
pub fn segment_starts(state: State<AppState>) -> Bytes {
|
||||
u32s(&state.read(|e| api::segment_starts(e).to_vec()))
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
pub fn segment_ids(state: State<AppState>) -> Vec<String> {
|
||||
state.read(api::segment_ids)
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
pub fn routing_revision(state: State<AppState>) -> u32 {
|
||||
state.read(api::routing_revision)
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
//! What the frontend knows of the storage, which is kept by the engine thread (see `state`).
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use gpx_engine::api;
|
||||
use serde::Serialize;
|
||||
use tauri::State;
|
||||
|
||||
use crate::state::AppState;
|
||||
|
||||
/// What opening the storage found.
|
||||
#[derive(Serialize)]
|
||||
pub struct StorageOpened {
|
||||
settings: BTreeMap<String, String>,
|
||||
/// All the files of the engine as added: the frontend starts with none.
|
||||
outcome: api::Outcome,
|
||||
}
|
||||
|
||||
/// The files were restored when the app started: the frontend is told about all of them.
|
||||
#[tauri::command(async)]
|
||||
pub fn open_storage(state: State<AppState>) -> StorageOpened {
|
||||
StorageOpened {
|
||||
settings: state.storage().settings(),
|
||||
outcome: state.read(api::Outcome::everything),
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolves when what was changed has been written to the storage, which is the case when a command
|
||||
/// returns.
|
||||
#[tauri::command(async)]
|
||||
pub fn flush_storage() {}
|
||||
|
||||
/// There is no other writer.
|
||||
#[tauri::command(async)]
|
||||
pub fn sync_storage() -> api::Outcome {
|
||||
api::Outcome::failed_none()
|
||||
}
|
||||
|
||||
/// Saves a setting, `value` being its JSON.
|
||||
#[tauri::command(async)]
|
||||
pub fn set_setting(state: State<AppState>, key: String, value: String) {
|
||||
state.put_setting(&key, &value);
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
pub fn delete_setting(state: State<AppState>, key: String) {
|
||||
state.delete_setting(&key);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
//! What the app asks of the system, outside of the files.
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use tauri::{AppHandle, Runtime};
|
||||
use tauri_plugin_opener::OpenerExt;
|
||||
|
||||
/// Opens a web page (or an email to write) with the app of the system for it: the links of the
|
||||
/// page are not followed in the window of the app. Only `http`, `https` and `mailto` addresses are
|
||||
/// opened.
|
||||
#[tauri::command(async)]
|
||||
pub fn open_url<R: Runtime>(app: AppHandle<R>, url: String) -> Result<(), String> {
|
||||
let allowed = ["http://", "https://", "mailto:"];
|
||||
if !allowed.iter().any(|scheme| url.starts_with(scheme)) {
|
||||
return Err(format!("{url} is not a web address"));
|
||||
}
|
||||
app.opener()
|
||||
.open_url(url, None::<&str>)
|
||||
.map_err(|error| error.to_string())
|
||||
}
|
||||
|
||||
/// Shows a file or a folder in the file manager of the system (the Finder...).
|
||||
#[tauri::command(async)]
|
||||
pub fn reveal_path<R: Runtime>(app: AppHandle<R>, path: PathBuf) -> Result<(), String> {
|
||||
app.opener()
|
||||
.reveal_item_in_dir(path)
|
||||
.map_err(|error| error.to_string())
|
||||
}
|
||||
Reference in new issue
Block a user