project file: 另存为 — duplicate the project in the library under a new name and switch to the copy
- save_project_as(name): library_duplicate the open project's row, rename the copy, open it as the current project - 文件 → 另存为 opens a name dialog (prefilled '<name> Copy'); OK duplicates + switches (the save-as semantics: a new, differently- named project in the library, editing continues on the copy)
This commit is contained in:
@@ -144,6 +144,8 @@ mod modal_ids {
|
||||
pub const EXPORT_PROJECT_DFD: usize = 19;
|
||||
/// The new-project dialog (文件 → 新建项目).
|
||||
pub const NEW_PROJECT: usize = 20;
|
||||
/// The save-as dialog (文件 → 另存为).
|
||||
pub const SAVE_AS: usize = 21;
|
||||
}
|
||||
|
||||
/// What a picked platform-dialog path should do.
|
||||
@@ -254,6 +256,11 @@ enum ModalState<E: AppEngine> {
|
||||
modal: Entity<Modal>,
|
||||
content: Entity<crate::dialogs::NewProjectContent>,
|
||||
},
|
||||
/// The save-as dialog (文件 → 另存为): the copy's name.
|
||||
SaveAs {
|
||||
modal: Entity<Modal>,
|
||||
content: Entity<crate::dialogs::RenameContent>,
|
||||
},
|
||||
}
|
||||
|
||||
/// A running export: the session the tick loop drains for progress.
|
||||
@@ -283,6 +290,7 @@ impl<E: AppEngine> ModalState<E> {
|
||||
| ModalState::EntryRename { modal, .. } => Some(modal.clone()),
|
||||
| ModalState::ExportProjectDfd { modal, .. } => Some(modal.clone()),
|
||||
| ModalState::NewProject { modal, .. } => Some(modal.clone()),
|
||||
| ModalState::SaveAs { modal, .. } => Some(modal.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1032,7 +1040,8 @@ impl<E: AppEngine> OakApp<E> {
|
||||
A::OpenFromLibrary | A::ProjectManager => self.show_project_manager(cx),
|
||||
A::Import => self.open_file_dialog(FileAction::ImportFootage, cx),
|
||||
// The 导出工程文件… dialog: choose OTIO / OVE / FCPXML + path.
|
||||
A::SaveProject | A::SaveProjectAs => self.open_export_project_dfd(cx),
|
||||
A::SaveProject => self.open_export_project_dfd(cx),
|
||||
A::SaveProjectAs => self.open_save_as(cx),
|
||||
A::CloseProject => self
|
||||
.engine
|
||||
.update(cx, |engine, cx| engine.close_project(cx)),
|
||||
@@ -2654,6 +2663,64 @@ impl<E: AppEngine> OakApp<E> {
|
||||
});
|
||||
}
|
||||
|
||||
/// Opens the 另存为 dialog: the copy's new name (prefilled with the
|
||||
/// current project name). OK duplicates the project into the library
|
||||
/// under that name and switches to the copy.
|
||||
fn open_save_as(&mut self, cx: &mut Context<Self>) {
|
||||
if !matches!(self.modal, ModalState::None) {
|
||||
return;
|
||||
}
|
||||
let current_name = self
|
||||
.engine
|
||||
.read(cx)
|
||||
.project()
|
||||
.map(|p| p.name.clone())
|
||||
.unwrap_or_else(|| crate::i18n::tr("manager.new.default_name").to_string());
|
||||
let mut name = current_name.clone();
|
||||
if let Some((stem, _)) = name.rsplit_once('.') {
|
||||
name = stem.to_string();
|
||||
}
|
||||
name = format!("{name} Copy");
|
||||
self.spawn_modal(cx, move |window, app| {
|
||||
let content = app.new(|cx| {
|
||||
crate::dialogs::RenameContent::new(name.clone().into(), window, cx)
|
||||
});
|
||||
let modal = app.new(|cx| {
|
||||
Modal::new(
|
||||
modal_ids::SAVE_AS,
|
||||
ModalOptions::new(
|
||||
crate::i18n::tr("menu.file.save_as"),
|
||||
px(420.0),
|
||||
)
|
||||
.with_button(DialogButton::primary(crate::i18n::tr("dialog.ok")))
|
||||
.with_button(DialogButton::new(
|
||||
crate::i18n::tr("dialog.cancel"),
|
||||
gpui_widgets::dialog::DialogButtonRole::Secondary,
|
||||
)),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.with_content(content.clone())
|
||||
});
|
||||
ModalState::SaveAs { modal, content }
|
||||
});
|
||||
}
|
||||
|
||||
/// Confirms the save-as dialog: duplicate + switch to the copy.
|
||||
fn commit_save_as(
|
||||
&mut self,
|
||||
content: &Entity<crate::dialogs::RenameContent>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let name = content.read(cx).value(cx).to_string();
|
||||
let engine = self.engine.clone();
|
||||
let result = engine.update(cx, |engine, cx| engine.save_project_as(&name, cx));
|
||||
if let Err(err) = result {
|
||||
println!("[file] save as failed: {err}");
|
||||
}
|
||||
self.close_modal(cx);
|
||||
}
|
||||
|
||||
/// Opens the multicam wizard (窗口 → 多机位向导): pick the angle
|
||||
/// footage, choose the sync mode, then create the multicam sequence.
|
||||
fn open_multicam_wizard(&mut self, cx: &mut Context<Self>) {
|
||||
@@ -3105,6 +3172,16 @@ impl<E: AppEngine> OakApp<E> {
|
||||
}
|
||||
}
|
||||
}
|
||||
modal_ids::SAVE_AS => {
|
||||
if let ModalState::SaveAs { content, .. } = &self.modal {
|
||||
if *button == 0 {
|
||||
let content = content.clone();
|
||||
self.commit_save_as(&content, cx);
|
||||
} else {
|
||||
self.close_modal(cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
modal_ids::SEQUENCE_PROPERTIES => {
|
||||
if let ModalState::SequenceProperties { content, .. } = &self.modal {
|
||||
let content = content.clone();
|
||||
|
||||
@@ -726,6 +726,14 @@ pub trait AppEngine:
|
||||
Err("project library not supported".into())
|
||||
}
|
||||
|
||||
/// 另存为: saves the OPEN project into the library as a differently-
|
||||
/// named copy and switches the current project to that copy. Default:
|
||||
/// unsupported.
|
||||
fn save_project_as(&mut self, name: &str, cx: &mut Context<Self>) -> Result<(), String> {
|
||||
let _ = (name, cx);
|
||||
Err("save-as not supported".into())
|
||||
}
|
||||
|
||||
/// Imports a `.ove` / `.otio` / `.fcpxml` project file into the library
|
||||
/// as a new row; returns the new row's uuid.
|
||||
fn library_import_project(&mut self, path: PathBuf) -> Result<String, String> {
|
||||
|
||||
@@ -5387,6 +5387,26 @@ impl AppEngine for RealEngine {
|
||||
.map_err(|e| format!("failed to duplicate the project: {e}"))
|
||||
}
|
||||
|
||||
/// 另存为: duplicate the open project into the library under a new
|
||||
/// (named) row, then open that copy as the current project. The
|
||||
/// duplicator applies the new name to the copy's row and projectname.
|
||||
fn save_project_as(&mut self, name: &str, cx: &mut Context<Self>) -> Result<(), String> {
|
||||
let Some(project) = self.project.clone() else {
|
||||
return Err("no project open".to_string());
|
||||
};
|
||||
let current_uuid = {
|
||||
let guard = graphops::lock(&project);
|
||||
guard.uuid.clone()
|
||||
};
|
||||
let new_uuid = graphops::library_duplicate(¤t_uuid)
|
||||
.map_err(|e| format!("duplicate failed: {e}"))?;
|
||||
graphops::library_rename(&new_uuid, name.trim())
|
||||
.map_err(|e| format!("rename copy failed: {e}"))?;
|
||||
// Open the new row (adopt switches the current project).
|
||||
self.open_library_project(&new_uuid, cx)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn library_import_project(&mut self, path: PathBuf) -> Result<String, String> {
|
||||
graphops::library_import(&path)
|
||||
.map_err(|e| format!("failed to import \"{}\": {e}", path.display()))
|
||||
|
||||
Reference in New Issue
Block a user