Merge pull request #1104 from eliasx45/upstream/import-project

This commit is contained in:
Brandon Thomas committed 2026-10-13 04:28:59 -04:00
commit 37b681ac12
11 files changed
+384 -30

No files matched your search

+18 -4
View File
@@ -245,6 +245,8 @@ struct PendingImport {
comps: Vec<u64>,
items: Vec<u64>,
errors: Vec<String>,
/// EffectCraft projects merged in (`file.importProject` results); never added to a comp.
projects: Vec<Value>,
}
/// Everything probing a file needs, apart from the session (it can run on a background thread).
@@ -370,10 +372,18 @@ fn prepare_import(s: &mut Session, p: &Value) -> Result<PendingImport> {
_ => return Err(bad("file.import", "page: a page number from 1")),
},
};
let mut paths = paths;
let mut comps = vec![];
let mut items = vec![];
let mut errors = vec![];
// EffectCraft projects import as projects, as After Effects imports an .aep.
let (project_paths, mut paths): (Vec<String>, Vec<String>) = paths.into_iter().partition(|x| super::import_project::is_project_path(x));
let mut projects = vec![];
for path in project_paths {
match s.execute("file.importProject", json!({"path": path})) {
Ok(r) => projects.push(r),
Err(e) => errors.push(format!("{path}: {e}")),
}
}
if let Some(retain) = retain {
let mut rest = vec![];
for path in paths {
@@ -424,13 +434,13 @@ fn prepare_import(s: &mut Session, p: &Value) -> Result<PendingImport> {
page,
psd_layer: p.get("layer").cloned(),
};
Ok(PendingImport { sources, prober, comps, items, errors })
Ok(PendingImport { sources, prober, comps, items, errors, projects })
}
/// The last part of an import: add the probed files to the project (one undo step) and select
/// everything the import made, so the Project panel shows it.
fn add_footage(s: &mut Session, pending: PendingImport, probed: Vec<std::result::Result<Probed, String>>) -> Result<Value> {
let PendingImport { comps, mut items, mut errors, .. } = pending;
let PendingImport { comps, mut items, mut errors, projects, .. } = pending;
let mut found = vec![];
let mut warnings = vec![];
for r in probed {
@@ -487,7 +497,11 @@ fn add_footage(s: &mut Session, pending: PendingImport, probed: Vec<std::result:
if let Some(first) = asked.first() {
s.events.push(crate::Event::Frontend { command: "file.interpretFootage".into(), params: json!({"item": first, "reason": "unlabeledAlpha"}) });
}
Ok(json!({"items": items, "comps": comps, "errors": errors, "warnings": warnings, "unlabeledAlpha": asked}))
let mut out = json!({"items": items, "comps": comps, "errors": errors, "warnings": warnings, "unlabeledAlpha": asked});
if !projects.is_empty() {
out["projects"] = Value::Array(projects);
}
Ok(out)
}
/// Whether a file format states how its alpha is stored (PNG, GIF, WebP and PSD are straight by
@@ -0,0 +1,143 @@
//! File ▸ Import ▸ EffectCraft Project: merge another `.ecproj` (or its XML copy) into the
//! current project, as After Effects imports an `.aep` into the open project. The imported
//! items land in a folder named after the file; every id is shifted past the current project's
//! so nothing collides, and the whole import is one undo step.
use effectcraft_color::Label;
use effectcraft_project::{ItemId, ItemKind, LayerSource, Project, essential};
use serde_json::{Value, json};
use super::{CommandSpec, always, bad, str_p};
use crate::{EngineError, Result, Session, cmd};
const C: &str = "file.importProject";
/// Whether `path` names a project EffectCraft imports as a project (not as footage).
pub(crate) fn is_project_path(path: &str) -> bool {
let lower = path.to_ascii_lowercase();
lower.ends_with(".ecproj") || lower.ends_with(".ecprojx")
}
/// Read a project file (JSON or its XML copy), as File ▸ Open does.
fn read_project(s: &Session, path: &str) -> Result<(Project, Option<String>)> {
let bytes = s.services.read_file(path).map_err(|e| EngineError::Other(format!("cannot read {path}: {e}")))?;
let text = String::from_utf8(bytes).map_err(|_| bad(C, format!("{path} is not a text project file")))?;
let text = if crate::xml_project::is_xml(&text) { crate::xml_project::from_xml(&text).map_err(EngineError::Other)? } else { text };
let project = Project::from_json(&text).map_err(|e| bad(C, format!("{path} is not a project EffectCraft can open: {e}")))?;
Ok((project, effectcraft_project::saved_by(&text)))
}
/// Footage paths relative to the imported project's folder become absolute when the file is
/// there; returns the footage paths that are missing.
fn resolve_footage(s: &Session, project: &mut Project, dir: &std::path::Path) -> Vec<String> {
let services = s.services.clone();
let resolve = |raw: &mut String| -> bool {
if raw.is_empty() || services.exists(raw) {
return true;
}
if std::path::Path::new(raw.as_str()).is_absolute() {
return false;
}
let joined = dir.join(raw.as_str()).to_string_lossy().to_string();
if services.exists(&joined) {
*raw = joined;
true
} else {
false
}
};
let mut missing = vec![];
for it in project.items.values_mut() {
if let ItemKind::Footage(f) = &mut it.kind {
if f.sequence.is_empty() {
if !resolve(&mut f.path) {
missing.push(f.path.clone());
}
} else {
let mut all = true;
for frame in &mut f.sequence {
all &= resolve(frame);
}
if !all {
missing.push(f.path.clone());
}
}
}
}
missing
}
/// The comps no other comp of `project` nests: the ones to open or place.
fn root_comps(project: &Project) -> Vec<ItemId> {
let nested: std::collections::HashSet<ItemId> =
project.comps().flat_map(|(_, c)| c.layers.iter().filter_map(|l| if let LayerSource::Comp { item } = l.source { Some(item) } else { None })).collect();
project.comps().map(|(id, _)| *id).filter(|id| !nested.contains(id)).collect()
}
fn import_project(s: &mut Session, p: &Value) -> Result<Value> {
let path = str_p(p, "path").ok_or_else(|| bad(C, "missing `path` (.ecproj)"))?.to_string();
let (mut sub, saved_by) = read_project(s, &path)?;
crate::upgrade_effects(&mut sub);
let file = std::path::Path::new(&path);
let dir = file.parent().map(|d| d.to_path_buf()).unwrap_or_default();
let missing = resolve_footage(s, &mut sub, &dir);
// The imported project's own render queue stays behind (as for templates).
let base = s.project.next_id;
if sub.next_id.checked_add(base).is_none() {
return Err(bad(C, format!("{path} has ids out of range")));
}
essential::offset_ids(&mut sub, base);
let roots: Vec<ItemId> = root_comps(&sub);
let folder_name = file.file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_else(|| "Imported Project".into());
let (folder, items) = s.edit("Import Project", None, |proj, st| {
let folder = proj.add_item(&folder_name, Label::Yellow, None, ItemKind::Folder);
let mut items = vec![];
for (id, mut it) in std::mem::take(&mut sub.items) {
if it.parent.is_none() {
it.parent = Some(folder);
}
items.push(id.0);
proj.items.insert(id, it);
}
proj.next_id = proj.next_id.max(sub.next_id);
proj.fix_next_id();
st.project_selection = vec![folder];
Ok((folder, items))
})?;
let comps: Vec<Value> = s
.project
.comps()
.filter(|(id, _)| items.contains(&id.0))
.map(|(id, _)| json!({"id": id.0, "name": s.project.item(*id).map(|i| i.name.clone()).unwrap_or_default()}))
.collect();
let primary = if let [one] = roots.as_slice() { Some(one.0) } else { None };
let mut warnings = vec![];
if let Some(v) = saved_by.as_deref().filter(|v| effectcraft_project::is_newer_version(v, effectcraft_project::APP_VERSION)) {
warnings.push(format!("saved by EffectCraft {v}, newer than this version ({})", effectcraft_project::APP_VERSION));
}
if !missing.is_empty() {
warnings.push(format!("{} footage file{} missing", missing.len(), if missing.len() == 1 { " is" } else { "s are" }));
}
s.toast(if warnings.is_empty() { format!("Imported {folder_name}") } else { format!("Imported {folder_name}: {}", warnings.join("; ")) });
Ok(json!({
"folder": folder.0,
"items": items,
"comps": comps,
"rootComps": roots.iter().map(|id| id.0).collect::<Vec<_>>(),
"primaryComp": primary,
"missingMedia": missing,
"warnings": warnings,
}))
}
pub fn specs() -> Vec<CommandSpec> {
vec![cmd!(
"file.importProject",
"EffectCraft Project...",
["File", "Import"],
None,
"{path (.ecproj or .ecprojx)} — merges the project into a folder named after the file; returns {folder, items, comps, rootComps, primaryComp (the only root comp, else null), missingMedia, warnings}",
always,
import_project
)]
}
+2
View File
@@ -26,6 +26,7 @@ pub mod footage_panel;
pub(crate) mod frame_export;
mod frontend;
mod help;
mod import_project;
mod key_labels;
mod key_transform;
mod keys_more;
@@ -197,6 +198,7 @@ pub fn command_specs() -> &'static [CommandSpec] {
v.extend(file_more::specs());
v.extend(relink::specs());
v.extend(lottie::specs());
v.extend(import_project::specs());
v.extend(timeline::specs());
v.extend(comp_more::specs());
v.extend(frame_export::specs());
+2
View File
@@ -1158,6 +1158,8 @@ mod tests_frame_export;
#[cfg(test)]
mod tests_fuzz_open;
#[cfg(test)]
mod tests_import_project;
#[cfg(test)]
mod tests_key_metadata_order;
#[cfg(test)]
mod tests_keylight;
+1
View File
@@ -538,6 +538,7 @@ File
File... | file.import
Multiple Files... | file.importMultiple
Adobe Premiere Pro Project... | file.importTimeline
EffectCraft Project... | file.importProject
Placeholder... | file.importPlaceholder
Solid... | file.importSolid
Lottie... | file.importLottie
+158
View File
@@ -0,0 +1,158 @@
//! File ▸ Import ▸ EffectCraft Project: another project merges into the open one with fresh ids,
//! its references intact, in one undo step.
use std::collections::HashSet;
use effectcraft_project::{Footage, FootageKind, ItemId, ItemKind, LayerSource, Project};
use serde_json::json;
use crate::Session;
fn out_dir(name: &str) -> std::path::PathBuf {
let d = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../target/test-out").join(name);
let _ = std::fs::remove_dir_all(&d);
std::fs::create_dir_all(&d).unwrap();
d
}
/// Every item, layer and property uid of `p` (they share one counter).
fn all_ids(p: &Project) -> Vec<u64> {
let mut v = vec![];
for (id, it) in &p.items {
v.push(id.0);
if let ItemKind::Comp(c) = &it.kind {
for l in &c.layers {
v.push(l.id.0);
fn uids(g: &effectcraft_project::PropGroup, v: &mut Vec<u64>) {
v.push(g.uid);
for c in &g.children {
match c {
effectcraft_project::Node::Prop(p) => v.push(p.uid),
effectcraft_project::Node::Group(g) => uids(g, v),
}
}
}
uids(&l.props, &mut v);
}
}
}
v
}
/// A project with "Main" nesting "Inner" under a parented null, a data file next to it by a
/// relative path and a footage file that's gone. Saved as `<dir>/Library Card.ecproj`.
fn source_project(dir: &std::path::Path) -> String {
let mut s = Session::default();
s.execute("comp.new", json!({"name": "Inner", "width": 64, "height": 64, "frameRate": 30, "duration": 2})).unwrap();
let inner = s.active_comp_id().unwrap();
s.execute("layer.newSolid", json!({"name": "Fill", "color": "#ff0000"})).unwrap();
s.execute("comp.new", json!({"name": "Main", "width": 64, "height": 64, "frameRate": 30, "duration": 2})).unwrap();
let main = s.active_comp_id().unwrap();
let nest = s.execute("layer.addItem", json!({"comp": main.0, "item": inner.0, "time": 0.0})).unwrap()["layer"].as_u64().unwrap();
let rig = s.execute("layer.newNull", json!({"name": "Rig"})).unwrap()["layer"].as_u64().unwrap();
s.execute("layer.setParent", json!({"layers": [nest], "parent": rig})).unwrap();
std::fs::write(dir.join("data.json"), b"{\"v\": 1}").unwrap();
let p = std::sync::Arc::make_mut(&mut s.project);
p.add_item("data.json", Default::default(), None, ItemKind::Footage(Footage { path: "data.json".into(), kind: FootageKind::Data, ..Default::default() }));
p.add_item(
"gone.mov",
Default::default(),
None,
ItemKind::Footage(Footage { path: "/offline/gone.mov".into(), kind: FootageKind::Video, ..Default::default() }),
);
let path = dir.join("Library Card.ecproj").to_string_lossy().to_string();
s.execute("file.saveAs", json!({"path": path})).unwrap();
path
}
#[test]
fn import_project_merges_with_fresh_ids_and_one_undo() {
let dir = out_dir("import-project");
let path = source_project(&dir);
let mut t = Session::default();
t.execute("comp.new", json!({"name": "Show", "width": 64, "height": 64, "frameRate": 30, "duration": 2})).unwrap();
t.execute("layer.newSolid", json!({"name": "Back", "color": "#00ff00"})).unwrap();
let before = t.project.items.len();
let before_ids: HashSet<u64> = all_ids(&t.project).into_iter().collect();
let r = t.execute("file.importProject", json!({"path": path})).unwrap();
let folder = ItemId(r["folder"].as_u64().unwrap());
assert_eq!(t.project.item(folder).unwrap().name, "Library Card.ecproj");
// Inner, Main, the Solids folder and its solid, two footage items, and the new folder.
assert_eq!(t.project.items.len(), before + 7);
let imported: Vec<ItemId> = r["items"].as_array().unwrap().iter().map(|v| ItemId(v.as_u64().unwrap())).collect();
// Everything lands in the folder (the Solids folder keeps its solid).
let top = |mut id: ItemId| {
while let Some(up) = t.project.item(id).and_then(|i| i.parent).filter(|p| *p != folder) {
id = up;
}
t.project.item(id).and_then(|i| i.parent)
};
assert!(imported.iter().all(|id| top(*id) == Some(folder)), "everything lands in the folder");
// No id is taken twice, and none of the open project's ids moved.
let ids = all_ids(&t.project);
let unique: HashSet<u64> = ids.iter().copied().collect();
assert_eq!(ids.len(), unique.len(), "ids collide");
assert!(before_ids.is_subset(&unique));
assert!(t.project.next_id > *ids.iter().max().unwrap());
// References follow the new ids: Main nests the imported Inner, under the imported null.
let main = ItemId(r["primaryComp"].as_u64().unwrap());
assert_eq!(t.project.item(main).unwrap().name, "Main");
assert_eq!(r["rootComps"], json!([main.0]));
let comp = t.project.comp(main).unwrap();
let nest = comp.layers.iter().find(|l| matches!(l.source, LayerSource::Comp { .. })).unwrap();
let LayerSource::Comp { item } = nest.source else { panic!("a precomp layer") };
assert_eq!(t.project.item(item).unwrap().name, "Inner");
assert!(imported.contains(&item));
let parent = nest.parent.unwrap();
assert_eq!(comp.layer(parent).unwrap().name, "Rig");
// A relative footage path resolves next to the imported project; a missing one is reported.
let data = t.project.items.values().find(|i| i.name == "data.json").unwrap();
let ItemKind::Footage(f) = &data.kind else { panic!("footage") };
assert_eq!(std::path::Path::new(&f.path), dir.join("data.json"));
assert_eq!(r["missingMedia"], json!(["/offline/gone.mov"]));
// One undo step removes all of it; redo brings it back.
t.execute("edit.undo", json!({})).unwrap();
assert_eq!(t.project.items.len(), before);
t.execute("edit.redo", json!({})).unwrap();
assert_eq!(t.project.items.len(), before + 7);
// Importing again keeps the copies apart, and File ▸ Import routes .ecproj here.
let r2 = t.execute("file.import", json!({"paths": [path]})).unwrap();
let again = &r2["projects"][0];
assert_ne!(again["primaryComp"], r["primaryComp"]);
assert!(r2["items"].as_array().unwrap().is_empty(), "a project is not footage");
let ids = all_ids(&t.project);
assert_eq!(ids.len(), ids.iter().copied().collect::<HashSet<_>>().len());
}
#[test]
fn import_project_rejects_bad_files_and_leaves_the_project() {
let dir = out_dir("import-project-bad");
let mut t = Session::default();
t.execute("comp.new", json!({"name": "Show", "width": 64, "height": 64, "frameRate": 30, "duration": 2})).unwrap();
let before = t.project.items.len();
std::fs::write(dir.join("junk.ecproj"), b"not a project").unwrap();
assert!(t.execute("file.importProject", json!({"path": dir.join("junk.ecproj").to_string_lossy()})).is_err());
assert!(t.execute("file.importProject", json!({"path": dir.join("absent.ecproj").to_string_lossy()})).is_err());
// Regression: an id at the top of the range saturated instead of overflowing (opening and
// importing must not panic), and the import refuses it.
let mut p = Project::default();
let id = p.add_item("Huge", Default::default(), None, ItemKind::Folder);
let mut it = p.items.remove(&id).unwrap();
it.id = ItemId(u64::MAX);
p.items.insert(it.id, it);
let huge = dir.join("huge.ecproj");
std::fs::write(&huge, p.to_json()).unwrap();
assert_eq!(Project::from_json(&p.to_json()).unwrap().next_id, u64::MAX);
let e = t.execute("file.importProject", json!({"path": huge.to_string_lossy()})).unwrap_err();
assert!(e.to_string().contains("out of range"), "{e}");
assert_eq!(t.project.items.len(), before);
}
+33 -24
View File
@@ -701,13 +701,13 @@ pub fn with_overrides(project: &Project, comp: ItemId, overrides: &[Override]) -
// ------------------------------------------------------------------------------ id remapping
/// Shift every id (items, layers, property uids, Essential Graphics controls) of `p` by
/// `offset`, keeping all references consistent: used when merging a template project into
/// another project.
/// `offset`, keeping all references consistent: used when merging a template or another project
/// into a project. Ids from a damaged file saturate instead of overflowing.
pub fn offset_ids(p: &mut Project, offset: u64) {
let items = std::mem::take(&mut p.items);
for (_, mut it) in items {
it.id = ItemId(it.id.0 + offset);
it.parent = it.parent.map(|f| ItemId(f.0 + offset));
it.id = ItemId(it.id.0.saturating_add(offset));
it.parent = it.parent.map(|f| ItemId(f.0.saturating_add(offset)));
if let ItemKind::Comp(c) = &mut it.kind {
let c = std::sync::Arc::make_mut(c);
for l in &mut c.layers {
@@ -716,18 +716,18 @@ pub fn offset_ids(p: &mut Project, offset: u64) {
if let Some(eg) = &mut c.essential {
fn go(v: &mut [EgControl], o: u64) {
for c in v {
c.id += o;
c.id = c.id.saturating_add(o);
match &mut c.kind {
EgKind::Property { layer, prop, links } => {
*layer = LayerId(layer.0 + o);
*prop += o;
*layer = LayerId(layer.0.saturating_add(o));
*prop = prop.saturating_add(o);
for k in links {
k.layer = LayerId(k.layer.0 + o);
k.prop += o;
k.layer = LayerId(k.layer.0.saturating_add(o));
k.prop = k.prop.saturating_add(o);
}
}
EgKind::Mirror { of } => *of += o,
EgKind::Media { layer } => *layer = LayerId(layer.0 + o),
EgKind::Mirror { of } => *of = of.saturating_add(o),
EgKind::Media { layer } => *layer = LayerId(layer.0.saturating_add(o)),
EgKind::Group { children } => go(children, o),
EgKind::Comment { .. } => {}
}
@@ -739,34 +739,43 @@ pub fn offset_ids(p: &mut Project, offset: u64) {
p.items.insert(it.id, it);
}
p.render_queue.clear();
p.next_id += offset;
p.next_id = p.next_id.saturating_add(offset);
}
fn offset_layer(l: &mut Layer, o: u64) {
l.id = LayerId(l.id.0 + o);
l.parent = l.parent.map(|x| LayerId(x.0 + o));
l.id = LayerId(l.id.0.saturating_add(o));
l.parent = l.parent.map(|x| LayerId(x.0.saturating_add(o)));
if let Some(tm) = &mut l.track_matte {
tm.layer = LayerId(tm.layer.0 + o);
tm.layer = LayerId(tm.layer.0.saturating_add(o));
}
match &mut l.source {
LayerSource::Footage { item } | LayerSource::Comp { item } | LayerSource::Solid { item } | LayerSource::Model { item } => *item = ItemId(item.0 + o),
LayerSource::Footage { item } | LayerSource::Comp { item } | LayerSource::Solid { item } | LayerSource::Model { item } => {
*item = ItemId(item.0.saturating_add(o))
}
_ => {}
}
fn group(g: &mut PropGroup, o: u64, essential_parent: bool) {
g.uid += o;
if let GroupKind::Essential { overridden } = &mut g.kind {
for u in overridden.iter_mut() {
*u += o;
g.uid = g.uid.saturating_add(o);
match &mut g.kind {
GroupKind::Essential { overridden } => {
for u in overridden.iter_mut() {
*u = u.saturating_add(o);
}
}
// A tracker's Motion Target is a layer of the same comp.
GroupKind::Tracker { settings } => {
settings.target = settings.target.map(|t| LayerId(t.0.saturating_add(o)));
}
_ => {}
}
let is_essential = matches!(g.kind, GroupKind::Essential { .. }) || essential_parent;
for c in &mut g.children {
match c {
Node::Prop(p) => {
p.uid += o;
p.uid = p.uid.saturating_add(o);
let fix = |v: &mut Value| {
if let Value::Layer(Some(id)) = v {
*id += o;
*id = id.saturating_add(o);
}
};
fix(&mut p.value);
@@ -774,7 +783,7 @@ fn offset_layer(l: &mut Layer, o: u64) {
fix(&mut k.value);
}
if is_essential && control_of(&p.match_id).is_some() {
p.match_id = match_id(control_of(&p.match_id).unwrap_or(0) + o);
p.match_id = match_id(control_of(&p.match_id).unwrap_or(0).saturating_add(o));
// Media replacement values are item ids.
if matches!(p.ui, ParamUi::Hidden)
&& let Value::Scalar(x) = &mut p.value
@@ -785,7 +794,7 @@ fn offset_layer(l: &mut Layer, o: u64) {
}
Node::Group(sub) => {
if is_essential && let Some(id) = control_of(&sub.match_id) {
sub.match_id = match_id(id + o);
sub.match_id = match_id(id.saturating_add(o));
}
group(sub, o, is_essential);
}
+3 -2
View File
@@ -1429,10 +1429,11 @@ impl Project {
pub fn fix_next_id(&mut self) {
let mut m = self.next_id;
for (id, it) in &self.items {
m = m.max(id.0 + 1);
// Saturating: a damaged file's ids must not overflow (the import then refuses it).
m = m.max(id.0.saturating_add(1));
if let ItemKind::Comp(c) = &it.kind {
for l in &c.layers {
m = m.max(l.id.0 + 1).max(l.props.max_uid() + 1);
m = m.max(l.id.0.saturating_add(1)).max(l.props.max_uid().saturating_add(1));
}
}
}
+10
View File
@@ -303,6 +303,7 @@ const SIMPLIFIED_CHINESE: &[(&str, &str, &str)] = &[
("file.import", "File...", "文件..."),
("file.importMultiple", "Multiple Files...", "多个文件..."),
("file.importTimeline", "Adobe Premiere Pro Project...", "Adobe Premiere Pro 项目..."),
("file.importProject", "EffectCraft Project...", "EffectCraft 项目..."),
("file.importPlaceholder", "Placeholder...", "占位符..."),
("file.importSolid", "Solid...", "纯色..."),
("file.importLottie", "Lottie...", "Lottie..."),
@@ -933,6 +934,7 @@ const TRADITIONAL_CHINESE: &[(&str, &str, &str)] = &[
("file.import", "File...", "檔案..."),
("file.importMultiple", "Multiple Files...", "多個檔案..."),
("file.importTimeline", "Adobe Premiere Pro Project...", "Adobe Premiere Pro 專案..."),
("file.importProject", "EffectCraft Project...", "EffectCraft 專案..."),
("file.importPlaceholder", "Placeholder...", "預留位置..."),
("file.importSolid", "Solid...", "純色..."),
("file.importLottie", "Lottie...", "Lottie..."),
@@ -1563,6 +1565,7 @@ const JAPANESE: &[(&str, &str, &str)] = &[
("file.importPlaceholder", "Placeholder...", "プレースホルダー..."),
("file.importSolid", "Solid...", "平面..."),
("file.importLottie", "Lottie...", "Lottie..."),
("file.importProject", "EffectCraft Project...", "EffectCraftプロジェクト..."),
("file.importVanishingPoint", "Vanishing Point (.vpe)...", "Vanishing Point (.vpe)..."),
("essential.importTemplate", "Essential Graphics Template...", "エッセンシャルグラフィックステンプレート..."),
("", "Import Recent Footage", "最近使用したフッテージを読み込み"),
@@ -2190,6 +2193,7 @@ const UKRAINIAN: &[(&str, &str, &str)] = &[
("file.import", "File...", "Файл..."),
("file.importMultiple", "Multiple Files...", "Кілька файлів..."),
("file.importTimeline", "Adobe Premiere Pro Project...", "Проєкт Adobe Premiere Pro..."),
("file.importProject", "EffectCraft Project...", "Проєкт EffectCraft..."),
("file.importPlaceholder", "Placeholder...", "Заповнювач..."),
("file.importSolid", "Solid...", "Суцільний колір..."),
("file.importLottie", "Lottie...", "Lottie..."),
@@ -2821,6 +2825,7 @@ const SPANISH: &[(&str, &str, &str)] = &[
("file.import", "File...", "Archivo..."),
("file.importMultiple", "Multiple Files...", "Varios archivos..."),
("file.importTimeline", "Adobe Premiere Pro Project...", "Proyecto de Adobe Premiere Pro..."),
("file.importProject", "EffectCraft Project...", "Proyecto de EffectCraft..."),
("file.importPlaceholder", "Placeholder...", "Marcador de posición..."),
("file.importSolid", "Solid...", "Sólido..."),
("file.importLottie", "Lottie...", "Lottie..."),
@@ -3450,6 +3455,7 @@ const PORTUGUESE: &[(&str, &str, &str)] = &[
("file.import", "File...", "Arquivo..."),
("file.importMultiple", "Multiple Files...", "Vários arquivos..."),
("file.importTimeline", "Adobe Premiere Pro Project...", "Projeto do Adobe Premiere Pro..."),
("file.importProject", "EffectCraft Project...", "Projeto do EffectCraft..."),
("file.importPlaceholder", "Placeholder...", "Espaço reservado..."),
("file.importSolid", "Solid...", "Sólido..."),
("file.importLottie", "Lottie...", "Lottie..."),
@@ -4087,6 +4093,7 @@ const RUSSIAN: &[(&str, &str, &str)] = &[
("file.import", "File...", "Файл..."),
("file.importMultiple", "Multiple Files...", "Несколько файлов..."),
("file.importTimeline", "Adobe Premiere Pro Project...", "Проект Adobe Premiere Pro..."),
("file.importProject", "EffectCraft Project...", "Проект EffectCraft..."),
("file.importPlaceholder", "Placeholder...", "Заполнитель..."),
("file.importSolid", "Solid...", "Сплошной цвет..."),
("file.importLottie", "Lottie...", "Lottie..."),
@@ -4716,6 +4723,7 @@ const FRENCH: &[(&str, &str, &str)] = &[
("file.import", "File...", "Fichier..."),
("file.importMultiple", "Multiple Files...", "Plusieurs fichiers..."),
("file.importTimeline", "Adobe Premiere Pro Project...", "Projet Adobe Premiere Pro..."),
("file.importProject", "EffectCraft Project...", "Projet EffectCraft..."),
("file.importPlaceholder", "Placeholder...", "Espace réservé..."),
("file.importSolid", "Solid...", "Solide..."),
("file.importLottie", "Lottie...", "Lottie..."),
@@ -5345,6 +5353,7 @@ const GERMAN: &[(&str, &str, &str)] = &[
("file.import", "File...", "Datei..."),
("file.importMultiple", "Multiple Files...", "Mehrere Dateien..."),
("file.importTimeline", "Adobe Premiere Pro Project...", "Adobe Premiere Pro-Projekt..."),
("file.importProject", "EffectCraft Project...", "EffectCraft-Projekt..."),
("file.importPlaceholder", "Placeholder...", "Platzhalter..."),
("file.importSolid", "Solid...", "Farbfläche..."),
("file.importLottie", "Lottie...", "Lottie..."),
@@ -5974,6 +5983,7 @@ const CZECH: &[(&str, &str, &str)] = &[
("file.import", "File...", "Soubor..."),
("file.importMultiple", "Multiple Files...", "Více souborů..."),
("file.importTimeline", "Adobe Premiere Pro Project...", "Projekt Adobe Premiere Pro..."),
("file.importProject", "EffectCraft Project...", "Projekt EffectCraft..."),
("file.importPlaceholder", "Placeholder...", "Zástupný záznam..."),
("file.importSolid", "Solid...", "Plná vrstva..."),
("file.importLottie", "Lottie...", "Lottie..."),
+1
View File
@@ -1326,6 +1326,7 @@ fn file_dialog(app: &mut EffectcraftApp, id: &str, params: &Value) -> Option<Res
"view.importGuides" => ("path", Ask::Open(&["json"])),
"file.exportLottie" => ("path", Ask::Save("Animation.json")),
"file.importLottie" => ("path", Ask::Open(&["json", "lottie"])),
"file.importProject" => ("path", Ask::Open(&["ecproj", "ecprojx"])),
"file.importTimeline" => ("path", Ask::Open(&["xml", "fcpxml", "otio", "edl", "aaf", "omf"])),
"render.saveCurrentPreview" => ("path", Ask::Save("Preview.mp4")),
"file.runScript" if params.get("name").is_none() => ("path", Ask::Open(&["jsx", "js", "jsonl", "json", "txt"])),
+13
View File
@@ -0,0 +1,13 @@
## Update: File ▸ Import ▸ EffectCraft Project
- **Import a project into a project**, as After Effects imports an `.aep`:
`file.importProject {path}` (File ▸ Import ▸ EffectCraft Project..., and `.ecproj` /
`.ecprojx` files given to File ▸ Import) merges the project into a folder named after the
file. Every item, layer and property id moves past the open project's, so references
(precomps, parents, track mattes, Layer Control values, Essential Graphics, tracker
targets) stay intact. It's one undo step. Footage paths relative to the imported project
resolve next to it. The result lists the comps, the root comps, `primaryComp` (the only
root comp) and missing media.
- Id shifting saturates instead of overflowing on damaged files, for both templates and
projects.