project file: new-project dialog (name + sequence format), open prompt lists formats

- 文件 → 新建项目 opens an in-app dialog: project name + the first
  sequence's format (preset / custom / interlaced, reusing
  SequenceFormatFields); OK creates the library project then seeds the
  first sequence from the chosen format
- 打开项目 prompt advertises the supported project formats
  (.ove / .otio / .fcpxml)
- manager.new.project_name across the 8 i18n packs
- action-search first-item test follows the new flow (New Project now
  opens its dialog)
This commit is contained in:
2026-09-02 09:15:30 +08:00
parent c900804941
commit 660a32a1b3
10 changed files with 153 additions and 20 deletions
+2 -1
View File
@@ -183,6 +183,7 @@
"manager.title": "Projektmanager"
"manager.new": "Neues Projekt"
"manager.new.default_name": "Unbenanntes Projekt"
"manager.new.project_name": "Projektname"
"manager.open": "Öffnen"
"manager.rename": "Umbenennen…"
"manager.rename.title": "Projekt umbenennen"
@@ -303,7 +304,7 @@
"about.description": "Oak Video Editor ist ein freier, quelloffener nichtlinearer Videoeditor. Diese Software ist unter der GNU GPL Version 3 lizenziert."
"about.thanks": "Besonderer Dank gilt Enzo GD, unserem Community Promoter, für seine großzügige Unterstützung bei der Verbreitung dieses Projekts in dessen Anfangszeit."
"about.fork_notice": "Dieses Projekt ist ein Fork des Olive Video Editors."
"file.open.title": "Projekt öffnen"
"file.open.title": "Projekt öffnen (.ove / .otio / .fcpxml)"
"file.import_footage.title": "Material importieren"
"preferences.title": "Einstellungen"
"preferences.section.general": "Allgemein"
+2 -1
View File
@@ -181,6 +181,7 @@
"manager.title": "Project Manager"
"manager.new": "New Project"
"manager.new.default_name": "Untitled Project"
"manager.new.project_name": "Project Name"
"manager.open": "Open"
"manager.rename": "Rename…"
"manager.rename.title": "Rename Project"
@@ -301,7 +302,7 @@
"about.description": "Oak Video Editor is a free open source non-linear video editor. This software is licensed under the GNU GPL Version 3."
"about.thanks": "Special thanks to Enzo GD, our Community Promoter, for his generous support in spreading the word about this project in its early days."
"about.fork_notice": "This project is a fork of Olive Video Editor."
"file.open.title": "Open Project"
"file.open.title": "Open Project (.ove / .otio / .fcpxml)"
"file.import_footage.title": "Import Footage"
"preferences.title": "Preferences"
"preferences.section.general": "General"
+2 -1
View File
@@ -183,6 +183,7 @@
"manager.title": "Administrador de proyectos"
"manager.new": "Nuevo proyecto"
"manager.new.default_name": "Proyecto sin título"
"manager.new.project_name": "Nombre del proyecto"
"manager.open": "Abrir"
"manager.rename": "Cambiar nombre…"
"manager.rename.title": "Cambiar nombre del proyecto"
@@ -303,7 +304,7 @@
"about.description": "Oak Video Editor es un editor de vídeo no lineal libre y de código abierto. Este software está licenciado bajo la GNU GPL versión 3."
"about.thanks": "Un agradecimiento especial a Enzo GD, nuestro Community Promoter, por su generoso apoyo al difundir este proyecto en sus primeros días."
"about.fork_notice": "Este proyecto es un fork de Olive Video Editor."
"file.open.title": "Abrir proyecto"
"file.open.title": "Abrir proyecto (.ove / .otio / .fcpxml)"
"file.import_footage.title": "Importar material"
"preferences.title": "Preferencias"
"preferences.section.general": "General"
+2 -1
View File
@@ -183,6 +183,7 @@
"manager.title": "Gestionnaire de projets"
"manager.new": "Nouveau projet"
"manager.new.default_name": "Projet sans titre"
"manager.new.project_name": "Nom du projet"
"manager.open": "Ouvrir"
"manager.rename": "Renommer…"
"manager.rename.title": "Renommer le projet"
@@ -303,7 +304,7 @@
"about.description": "Oak Video Editor est un éditeur vidéo non linéaire libre et open source. Ce logiciel est sous licence GNU GPL version 3."
"about.thanks": "Remerciements particuliers à Enzo GD, notre Community Promoter, pour son généreux soutien à la diffusion de ce projet à ses débuts."
"about.fork_notice": "Ce projet est un fork d'Olive Video Editor."
"file.open.title": "Ouvrir le projet"
"file.open.title": "Ouvrir le projet (.ove / .otio / .fcpxml)"
"file.import_footage.title": "Importer des médias"
"preferences.title": "Préférences"
"preferences.section.general": "Général"
+2 -1
View File
@@ -183,6 +183,7 @@
"manager.title": "プロジェクトマネージャー"
"manager.new": "新規プロジェクト"
"manager.new.default_name": "無題のプロジェクト"
"manager.new.project_name": "プロジェクト名"
"manager.open": "開く"
"manager.rename": "名前を変更…"
"manager.rename.title": "プロジェクトの名前を変更"
@@ -303,7 +304,7 @@
"about.description": "Oak Video Editor はフリーでオープンソースのノンリニア動画エディタです。本ソフトウェアは GNU GPL バージョン 3 でライセンスされています。"
"about.thanks": "プロジェクト初期に広く紹介してくれたコミュニティプロモーター Enzo GD に心より感謝します。"
"about.fork_notice": "本プロジェクトは Olive Video Editor のフォークです。"
"file.open.title": "プロジェクトを開く"
"file.open.title": "プロジェクトを開く.ove / .otio / .fcpxml"
"file.import_footage.title": "素材を読み込み"
"preferences.title": "環境設定"
"preferences.section.general": "一般"
+2 -1
View File
@@ -183,6 +183,7 @@
"manager.title": "Gerenciador de projetos"
"manager.new": "Novo projeto"
"manager.new.default_name": "Projeto sem título"
"manager.new.project_name": "Nome do projeto"
"manager.open": "Abrir"
"manager.rename": "Renomear…"
"manager.rename.title": "Renomear projeto"
@@ -303,7 +304,7 @@
"about.description": "O Oak Video Editor é um editor de vídeo não linear livre e de código aberto. Este software é licenciado sob a GNU GPL versão 3."
"about.thanks": "Um agradecimento especial a Enzo GD, nosso Community Promoter, por seu generoso apoio na divulgação deste projeto em seus primórdios."
"about.fork_notice": "Este projeto é um fork do Olive Video Editor."
"file.open.title": "Abrir projeto"
"file.open.title": "Abrir projeto (.ove / .otio / .fcpxml)"
"file.import_footage.title": "Importar mídia"
"preferences.title": "Preferências"
"preferences.section.general": "Geral"
+2 -1
View File
@@ -183,6 +183,7 @@
"manager.title": "Диспетчер проектов"
"manager.new": "Новый проект"
"manager.new.default_name": "Проект без названия"
"manager.new.project_name": "Название проекта"
"manager.open": "Открыть"
"manager.rename": "Переименовать…"
"manager.rename.title": "Переименовать проект"
@@ -303,7 +304,7 @@
"about.description": "Oak Video Editor — свободный нелинейный видеоредактор с открытым исходным кодом. Программа распространяется по лицензии GNU GPL версии 3."
"about.thanks": "Особая благодарность Enzo GD, нашему Community Promoter, за щедрую поддержку в распространении этого проекта в первые дни."
"about.fork_notice": "Этот проект является форком Olive Video Editor."
"file.open.title": "Открыть проект"
"file.open.title": "Открыть проект (.ove / .otio / .fcpxml)"
"file.import_footage.title": "Импорт медиафайлов"
"preferences.title": "Настройки"
"preferences.section.general": "Общие"
+2 -1
View File
@@ -181,6 +181,7 @@
"manager.title": "项目管理器"
"manager.new": "新建项目"
"manager.new.default_name": "未命名项目"
"manager.new.project_name": "项目名称"
"manager.open": "打开"
"manager.rename": "重命名…"
"manager.rename.title": "重命名工程"
@@ -301,7 +302,7 @@
"about.description": "Oak 视频编辑器是一款自由开源的非线性视频编辑器。本软件以 GNU GPL 第三版许可证发布。"
"about.thanks": "特别感谢我们的社区推广者 Enzo GD,感谢他在项目早期为传播本项目所给予的慷慨支持。"
"about.fork_notice": "本项目是 Olive 视频编辑器的 fork。"
"file.open.title": "打开项目"
"file.open.title": "打开项目.ove / .otio / .fcpxml"
"file.import_footage.title": "导入素材"
"preferences.title": "偏好设置"
"preferences.section.general": "常规"
+79 -12
View File
@@ -142,6 +142,8 @@ mod modal_ids {
pub const RENAME: usize = 18;
/// The project export dialog (文件 → 导出工程文件…).
pub const EXPORT_PROJECT_DFD: usize = 19;
/// The new-project dialog (文件 → 新建项目).
pub const NEW_PROJECT: usize = 20;
}
/// What a picked platform-dialog path should do.
@@ -247,6 +249,11 @@ enum ModalState<E: AppEngine> {
modal: Entity<Modal>,
content: Entity<crate::dialogs::ExportProjectDialogContent>,
},
/// The new-project dialog (文件 → 新建项目): name + sequence format.
NewProject {
modal: Entity<Modal>,
content: Entity<crate::dialogs::NewProjectContent>,
},
}
/// A running export: the session the tick loop drains for progress.
@@ -275,6 +282,7 @@ impl<E: AppEngine> ModalState<E> {
| ModalState::MulticamWizard { modal, .. }
| ModalState::EntryRename { modal, .. } => Some(modal.clone()),
| ModalState::ExportProjectDfd { modal, .. } => Some(modal.clone()),
| ModalState::NewProject { modal, .. } => Some(modal.clone()),
}
}
}
@@ -1019,7 +1027,7 @@ impl<E: AppEngine> OakApp<E> {
use crate::actions::ActionId as A;
match action {
// --- File ------------------------------------------------------
A::NewProject => self.new_project(cx),
A::NewProject => self.open_new_project(cx),
A::OpenProject => self.open_file_dialog(FileAction::Open, cx),
A::OpenFromLibrary | A::ProjectManager => self.show_project_manager(cx),
A::Import => self.open_file_dialog(FileAction::ImportFootage, cx),
@@ -1257,14 +1265,61 @@ impl<E: AppEngine> OakApp<E> {
/// 新建项目: creates a blank project in the library and opens it (the
/// write-through persists it from the first edit). Falls back to the
/// engine's plain new-project path when the library is unavailable.
fn new_project(&mut self, cx: &mut Context<Self>) {
let name = crate::i18n::tr("manager.new.default_name").to_string();
let result = self
.engine
.update(cx, |engine, cx| engine.library_create_project(&name, cx));
if let Err(err) = result {
println!("[file] library create failed ({err}); plain new project");
self.engine.update(cx, |engine, cx| engine.new_project(cx));
fn open_new_project(&mut self, cx: &mut Context<Self>) {
if !matches!(self.modal, ModalState::None) {
return;
}
self.spawn_modal(cx, move |window, app| {
let content = app.new(|cx| crate::dialogs::NewProjectContent::new(window, cx));
let modal = app.new(|cx| {
Modal::new(
modal_ids::NEW_PROJECT,
ModalOptions::new(
crate::i18n::tr("menu.file.new_project"),
px(480.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::NewProject { modal, content }
});
}
/// Creates the project from the dialog's name + format, then seeds its
/// first sequence (the old `new_project` path kept for manager fallback).
fn commit_new_project(&mut self, content: &Entity<crate::dialogs::NewProjectContent>, cx: &mut Context<Self>) {
let name = content.read(cx).name(cx).to_string();
let format = content.read(cx).format(cx);
let interlaced = content.read(cx).interlaced(cx);
let engine = self.engine.clone();
let result = engine.update(cx, |engine, cx| {
engine.library_create_project(&name, cx)
});
match result {
Ok(()) => {
self.close_modal(cx);
let engine = self.engine.clone();
engine.update(cx, |engine, cx| {
let _ = engine.create_sequence_with_params(
crate::i18n::tr("manager.new.default_name").to_string(),
format,
interlaced,
cx,
);
});
}
Err(err) => {
println!("[file] library create failed ({err}); plain new project");
self.close_modal(cx);
self.engine.update(cx, |engine, cx| engine.new_project(cx));
}
}
}
@@ -3040,6 +3095,16 @@ impl<E: AppEngine> OakApp<E> {
}
}
}
modal_ids::NEW_PROJECT => {
if let ModalState::NewProject { content, .. } = &self.modal {
if *button == 0 {
let content = content.clone();
self.commit_new_project(&content, cx);
} else {
self.close_modal(cx);
}
}
}
modal_ids::SEQUENCE_PROPERTIES => {
if let ModalState::SequenceProperties { content, .. } = &self.modal {
let content = content.clone();
@@ -4845,15 +4910,17 @@ mod tests {
);
// Down selects the first listed action, Enter runs it. The empty query
// lists every menu-bar action, so the first one is New Project.
// lists every menu-bar action, so the first one is New Project — the
// v0.5 flow opens the new-project dialog (name + format), so the
// search dialog hands off to it.
cx.dispatch_keystroke(window.into(), gpui::Keystroke::parse("down").unwrap());
cx.run_until_parked();
cx.dispatch_keystroke(window.into(), gpui::Keystroke::parse("enter").unwrap());
cx.run_until_parked();
assert!(
cx.read(|app| matches!(root.read(app).modal, ModalState::None)),
"executing the selected action closes the dialog"
cx.read(|app| matches!(root.read(app).modal, ModalState::NewProject { .. })),
"the search dialog hands off to the new-project dialog"
);
}
+58
View File
@@ -3952,6 +3952,64 @@ impl Render for ExportProjectDialogContent {
}
}
/// The new-project dialog (文件 → 新建项目): project name + the first
/// sequence's format (preset / custom width-height-rate / interlaced).
/// The host reads [`Self::name`] + [`Self::format`]; the engine's
/// library_create_project then seeds the first sequence.
pub struct NewProjectContent {
name: Entity<TextValue>,
format: Entity<SequenceFormatFields>,
}
impl NewProjectContent {
/// Builds the dialog (default name, HD 1080p25).
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let name = cx.new(|cx| {
let editor = cx.new(|cx| EditableTextState::new(StringStorage::default(), cx));
TextValue { editor }
});
name.update(cx, |field, cx| {
field.set_value(i18n::tr("manager.new.default_name"), cx)
});
let format = cx.new(|cx| {
SequenceFormatFields::build(SequenceFormatSeed::hd_1080p25(), window, cx)
});
Self { name, format }
}
/// The project name entered.
pub fn name(&self, cx: &App) -> SharedString {
self.name.read(cx).value(cx)
}
/// The format selected.
pub fn format(&self, cx: &App) -> crate::oakui::engine::VideoFormat {
self.format.read(cx).format(cx)
}
/// The interlaced flag.
pub fn interlaced(&self, cx: &App) -> bool {
self.format.read(cx).interlaced(cx)
}
}
impl Render for NewProjectContent {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let colors = cx.default_colors().clone();
div()
.flex()
.flex_col()
.gap_3()
.w_full()
.child(form_row(
&colors,
i18n::tr("manager.new.project_name").into(),
self.name.clone(),
))
.child(self.format.read(cx).rows(&colors))
}
}
#[cfg(test)]
mod tests {
use super::*;