mirror of
https://github.com/storytold/filmcraft.git
synced 2026-10-30 04:57:19 -06:00
Keep timeline clips when Project Clear removes used media.
Clear used to strip every track item that referenced a deleted Project file. Those clips now stay on the sequence as Media Offline, and media.relink still finds the item. Unused media, bins, and generators are still removed.
This commit is contained in:
@@ -2820,20 +2820,48 @@ fn build() -> Vec<CommandSpec> {
|
||||
}
|
||||
items.sort_by_key(|i| i.0);
|
||||
items.dedup();
|
||||
let mut used = std::collections::HashSet::new();
|
||||
for it in s.project.sequences() {
|
||||
if let Some(q) = it.as_sequence() {
|
||||
for t in q.all_tracks() {
|
||||
used.extend(t.items.iter().map(|x| x.item));
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut gone = Vec::new();
|
||||
let mut offlined = Vec::new();
|
||||
for i in items {
|
||||
let keep = used.contains(&i)
|
||||
&& s.project.item(i).and_then(filmcraft_project::ProjectItem::as_media).is_some_and(|m| matches!(m.media, MediaRef::File { .. }));
|
||||
if keep {
|
||||
offlined.push(i);
|
||||
} else {
|
||||
gone.push(i);
|
||||
}
|
||||
}
|
||||
s.edit("Clear", |pr, st| {
|
||||
for i in &offlined {
|
||||
if let Some(m) = pr.item_mut(*i).and_then(filmcraft_project::ProjectItem::as_media_mut) {
|
||||
m.offline = true;
|
||||
}
|
||||
}
|
||||
for b in &bins {
|
||||
pr.root.remove_bin(*b);
|
||||
}
|
||||
for i in &items {
|
||||
for i in &offlined {
|
||||
if pr.root.parent_of(*i).is_none() {
|
||||
pr.root.children.push(filmcraft_project::BinEntry::Item(*i));
|
||||
}
|
||||
}
|
||||
for i in &gone {
|
||||
pr.items.remove(i);
|
||||
pr.root.remove_item(*i);
|
||||
}
|
||||
// remove track items that referenced deleted media
|
||||
let seq_ids: Vec<ItemId> = pr.sequences().map(|q| q.id).collect();
|
||||
for sid in seq_ids {
|
||||
if let Some(q) = pr.sequence_mut(sid) {
|
||||
for t in q.all_tracks_mut() {
|
||||
t.items.retain(|x| !items.contains(&x.item));
|
||||
t.items.retain(|x| !gone.contains(&x.item));
|
||||
filmcraft_edit::remove_orphan_transitions(t);
|
||||
}
|
||||
}
|
||||
@@ -2841,11 +2869,12 @@ fn build() -> Vec<CommandSpec> {
|
||||
st.project_selection.clear();
|
||||
Ok(())
|
||||
})?;
|
||||
for i in &items {
|
||||
for i in gone.iter().chain(&offlined) {
|
||||
s.media.remove(*i);
|
||||
}
|
||||
crate::relink::refresh(s);
|
||||
s.fix_state();
|
||||
Ok(json!({"items": items.len(), "bins": bins.len()}))
|
||||
Ok(json!({"items": gone.len(), "bins": bins.len(), "offline": offlined.len()}))
|
||||
}),
|
||||
cmd!("project.moveToBin", "Move to Bin", [], None, r#"{"items":[id|binId]?,"bin":binId|null}"#, always, |s, p| {
|
||||
let items: Vec<ItemId> = match p.get("items").and_then(Value::as_array) {
|
||||
|
||||
@@ -234,15 +234,15 @@ fn project_delete_takes_explicit_bins_and_items_without_a_selection() {
|
||||
assert_eq!(disabled(s.execute("project.delete", json!({}))), "select an item in the Project panel");
|
||||
// an empty bin, by its id alone
|
||||
let r = s.execute("project.delete", json!({"items": [empty]})).unwrap();
|
||||
assert_eq!(r, json!({"items": 0, "bins": 1}));
|
||||
assert_eq!(r, json!({"items": 0, "bins": 1, "offline": 0}));
|
||||
assert!(s.project.root.find_bin(filmcraft_project::BinId(empty)).is_none());
|
||||
// a bin with a clip in it, and an item, each by id alone
|
||||
let r = s.execute("project.delete", json!({"items": [outer]})).unwrap();
|
||||
assert_eq!(r, json!({"items": 1, "bins": 1}));
|
||||
assert_eq!(r, json!({"items": 1, "bins": 1, "offline": 0}));
|
||||
assert!(s.project.item(ocean).is_none());
|
||||
let dunes = item_named(&s, "Desert_Dunes.mp4");
|
||||
let r = s.execute("project.delete", json!({"items": [dunes.0]})).unwrap();
|
||||
assert_eq!(r, json!({"items": 1, "bins": 0}));
|
||||
assert_eq!(r, json!({"items": 1, "bins": 0, "offline": 0}));
|
||||
assert_eq!(s.project.items.len(), before - 2);
|
||||
assert!(s.state.project_selection.is_empty(), "the selection is left as it was");
|
||||
assert!(!s.is_enabled("project.delete"), "menu enablement still follows the selection");
|
||||
|
||||
@@ -469,13 +469,13 @@ fn clear_deletes_a_bin_with_everything_in_it() {
|
||||
select(&mut s);
|
||||
// an empty bin, by its id: gone, nothing else touched
|
||||
let r = s.execute("project.delete", json!({"items": [empty]})).unwrap();
|
||||
assert_eq!(r, json!({"items": 0, "bins": 1}));
|
||||
assert_eq!(r, json!({"items": 0, "bins": 1, "offline": 0}));
|
||||
assert!(s.project.root.find_bin(filmcraft_project::BinId(empty)).is_none());
|
||||
assert_eq!(s.project.items.len(), before);
|
||||
// a bin holding a sub-bin holding a clip: all three go
|
||||
select(&mut s);
|
||||
let r = s.execute("project.delete", json!({"items": [outer]})).unwrap();
|
||||
assert_eq!(r, json!({"items": 1, "bins": 1}));
|
||||
assert_eq!(r, json!({"items": 1, "bins": 1, "offline": 0}));
|
||||
assert!(s.project.root.find_bin(filmcraft_project::BinId(outer)).is_none());
|
||||
assert!(s.project.root.find_bin(filmcraft_project::BinId(inner)).is_none());
|
||||
assert!(s.project.item(ocean).is_none());
|
||||
@@ -487,7 +487,7 @@ fn clear_deletes_a_bin_with_everything_in_it() {
|
||||
// the clip, its bin and the bin around it all named: each counted once
|
||||
select(&mut s);
|
||||
let r = s.execute("project.delete", json!({"items": [ocean.0, inner, outer]})).unwrap();
|
||||
assert_eq!(r, json!({"items": 1, "bins": 1}));
|
||||
assert_eq!(r, json!({"items": 1, "bins": 1, "offline": 0}));
|
||||
assert_eq!(s.project.items.len(), before - 1);
|
||||
s.execute("edit.undo", json!({})).unwrap();
|
||||
// the project's own top bin is never cleared
|
||||
@@ -496,3 +496,27 @@ fn clear_deletes_a_bin_with_everything_in_it() {
|
||||
s.execute("project.delete", json!({"items": [root]})).unwrap();
|
||||
assert_eq!(s.project.items.len(), before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deleting_media_used_on_the_timeline_makes_it_offline() {
|
||||
let mut s = Session::default();
|
||||
s.execute("file.newProject", json!({"name": "Offline"})).unwrap();
|
||||
s.execute("file.newSequence", json!({"name": "Seq", "fps": 25, "video": 1, "audio": 0, "width": 16, "height": 16})).unwrap();
|
||||
let item = s.execute("file.newOfflineFile", json!({"name": "Clip", "seconds": 2, "fps": 25, "video": true, "audio": false, "width": 16, "height": 16})).unwrap()["item"]
|
||||
.as_u64()
|
||||
.unwrap();
|
||||
s.execute("timeline.place", json!({"item": item, "track": "V1", "time": 0})).unwrap();
|
||||
let clips_before = s.project.sequences().next().unwrap().as_sequence().unwrap().video_tracks[0].items.len();
|
||||
let r = s.execute("project.delete", json!({"items": [item]})).unwrap();
|
||||
assert_eq!(r["offline"], 1);
|
||||
assert_eq!(r["items"], 0);
|
||||
assert!(s.project.item(ItemId(item)).unwrap().as_media().unwrap().offline);
|
||||
assert_eq!(s.project.sequences().next().unwrap().as_sequence().unwrap().video_tracks[0].items.len(), clips_before);
|
||||
let unused = s.execute("file.newOfflineFile", json!({"name": "Spare", "seconds": 1, "fps": 25, "video": true, "audio": false, "width": 16, "height": 16})).unwrap()["item"]
|
||||
.as_u64()
|
||||
.unwrap();
|
||||
let r = s.execute("project.delete", json!({"items": [unused]})).unwrap();
|
||||
assert_eq!(r["items"], 1);
|
||||
assert_eq!(r["offline"], 0);
|
||||
assert!(s.project.item(ItemId(unused)).is_none());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user