mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-02 22:42:24 -06:00
G2: distinct XML copy name and a name prompt for web Save As (#2788)
This commit is contained in:
@@ -213,8 +213,17 @@ fn install_hooks(app: &mut EffectcraftApp) {
|
||||
}
|
||||
None
|
||||
}));
|
||||
// Saving downloads the file: the chosen name is all a "save dialog" needs.
|
||||
app.hooks.pick_save = Some(Box::new(|name: &str| Some(files::path_for(if name.is_empty() { "Untitled Project.ecproj" } else { name }))));
|
||||
// Saving downloads the file: the chosen name is all a "save dialog" needs. A project (Save As,
|
||||
// Save a Copy) asks for the name, starting from the open project's (#2788): without asking,
|
||||
// Save As would replace the open project's file; Cancel cancels.
|
||||
app.hooks.pick_save = Some(Box::new(|name: &str| {
|
||||
let name = if name.is_empty() { "Untitled Project.ecproj" } else { name };
|
||||
if !std::path::Path::new(name).extension().is_some_and(|e| e.eq_ignore_ascii_case("ecproj") || e.eq_ignore_ascii_case("ecprojx")) {
|
||||
return Some(files::path_for(name));
|
||||
}
|
||||
let typed = web_sys::window().and_then(|w| w.prompt_with_message_and_default("Save project as:", name).ok().flatten());
|
||||
crate::store::typed_save_name(name, typed).map(|n| files::path_for(&n))
|
||||
}));
|
||||
app.hooks.audio_device = Some(Box::new(audio::open));
|
||||
app.hooks.audio_devices = Some(Box::new(|| vec!["Browser default output".into()]));
|
||||
}
|
||||
|
||||
@@ -152,6 +152,19 @@ pub fn key_path(key: &str) -> Option<&str> {
|
||||
key.strip_prefix(FILES).filter(|p| p.starts_with('/'))
|
||||
}
|
||||
|
||||
/// The project file name typed in the web's Save As prompt (#2788): `None` when it was cancelled or
|
||||
/// left empty; folders are dropped (the file table is flat) and a name without an extension gets
|
||||
/// the default's (`Intro copy` → `Intro copy.ecproj`).
|
||||
pub fn typed_save_name(default: &str, typed: Option<String>) -> Option<String> {
|
||||
let typed = typed?;
|
||||
let name = typed.trim().rsplit(['/', '\\']).next().unwrap_or("").trim();
|
||||
if name.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let ext = Path::new(default).extension().map(|e| e.to_string_lossy().to_string()).unwrap_or_default();
|
||||
if ext.is_empty() || Path::new(name).extension().is_some() { Some(name.to_string()) } else { Some(format!("{name}.{ext}")) }
|
||||
}
|
||||
|
||||
/// FNV-1a 64. Computed when a disk-cache key first needs the file, not on every write.
|
||||
fn content_hash(data: &[u8]) -> u64 {
|
||||
let mut h: u64 = 0xcbf2_9ce4_8422_2325;
|
||||
@@ -436,6 +449,17 @@ pub struct SnapshotMeta {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn typed_save_names() {
|
||||
let t = |s: &str| Some(s.to_string());
|
||||
assert_eq!(typed_save_name("Intro.ecproj", t("Intro copy")).as_deref(), Some("Intro copy.ecproj"));
|
||||
assert_eq!(typed_save_name("Intro.ecproj", t(" Other.ecproj ")).as_deref(), Some("Other.ecproj"));
|
||||
assert_eq!(typed_save_name("Intro.ecproj", t("a/b\\Shot.ecproj")).as_deref(), Some("Shot.ecproj"));
|
||||
assert_eq!(typed_save_name("Intro.ecproj", t(" ")), None);
|
||||
assert_eq!(typed_save_name("Intro.ecproj", t("folder/")), None);
|
||||
assert_eq!(typed_save_name("Intro.ecproj", None), None);
|
||||
}
|
||||
|
||||
fn store() -> Arc<Store> {
|
||||
let s = Arc::new(Store::default());
|
||||
let _ = s.clock.set(|| 1_000.0);
|
||||
|
||||
@@ -585,7 +585,11 @@ pub(super) fn file_dialog(app: &mut EffectcraftApp, id: &str, params: &Value) ->
|
||||
"file.replaceFootage" => ("path", Ask::Import),
|
||||
"file.relinkFootage" => ("folder", Ask::Folder),
|
||||
"file.collectFiles" => ("folder", Ask::Save("Collected Files".into())),
|
||||
"file.saveCopyAsXml" => ("path", Ask::Save(project_as("", "ecprojx"))),
|
||||
// An open XML project gets ` copy` too: the copy never writes over the open file.
|
||||
"file.saveCopyAsXml" => {
|
||||
let xml = app.session.path.as_deref().and_then(|p| std::path::Path::new(p).extension()).is_some_and(|e| e.eq_ignore_ascii_case("ecprojx"));
|
||||
("path", Ask::Save(project_as(if xml { " copy" } else { "" }, "ecprojx")))
|
||||
}
|
||||
"keys.rpfCameraImport" => ("path", Ask::Open(&["json", "csv", "txt"])),
|
||||
"essential.exportTemplate" => ("path", Ask::Save("Template.ectemplate".into())),
|
||||
"essential.importTemplate" => ("path", Ask::Open(&["ectemplate"])),
|
||||
|
||||
@@ -328,6 +328,13 @@ fn save_as_dialogs_start_from_the_open_project() {
|
||||
let xml = dir.join("Intro.ecprojx").to_string_lossy().to_string();
|
||||
// Save a Copy names another file: it would refuse to write over the open project.
|
||||
assert_eq!(*asked.borrow(), ["project Intro.ecproj".to_string(), "project Intro copy.ecproj".into(), format!("file {xml} [ecprojx]")]);
|
||||
// An open XML project's XML copy is another file too.
|
||||
app.session.path = Some(dir.join("Intro.ecprojx").to_string_lossy().to_string());
|
||||
asked.borrow_mut().clear();
|
||||
invoke(&mut app, &ctx, "file.saveCopyAsXml", json!({})).unwrap();
|
||||
let copy = dir.join("Intro copy.ecprojx").to_string_lossy().to_string();
|
||||
assert_eq!(*asked.borrow(), [format!("file {copy} [ecprojx]")]);
|
||||
app.session.path = Some(project.to_string_lossy().to_string());
|
||||
// The non-blocking dialog gets the whole path, so it opens in the project's folder.
|
||||
let pending: Arc<Mutex<Vec<crate::FileDialog>>> = Arc::default();
|
||||
let queue = pending.clone();
|
||||
|
||||
Reference in New Issue
Block a user