diff --git a/assets/i18n/de.yaml b/assets/i18n/de.yaml index e7a385986..e3d4ce823 100644 --- a/assets/i18n/de.yaml +++ b/assets/i18n/de.yaml @@ -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" diff --git a/assets/i18n/en-US.yaml b/assets/i18n/en-US.yaml index 42f081fb6..fe8101a0c 100644 --- a/assets/i18n/en-US.yaml +++ b/assets/i18n/en-US.yaml @@ -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" diff --git a/assets/i18n/es.yaml b/assets/i18n/es.yaml index ee5c8ca84..c4d5a2cba 100644 --- a/assets/i18n/es.yaml +++ b/assets/i18n/es.yaml @@ -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" diff --git a/assets/i18n/fr.yaml b/assets/i18n/fr.yaml index 9c3b20538..0fcd6eec5 100644 --- a/assets/i18n/fr.yaml +++ b/assets/i18n/fr.yaml @@ -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" diff --git a/assets/i18n/ja.yaml b/assets/i18n/ja.yaml index a337aeded..459a461bb 100644 --- a/assets/i18n/ja.yaml +++ b/assets/i18n/ja.yaml @@ -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": "一般" diff --git a/assets/i18n/pt.yaml b/assets/i18n/pt.yaml index 45a87b2be..a6891f5a5 100644 --- a/assets/i18n/pt.yaml +++ b/assets/i18n/pt.yaml @@ -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" diff --git a/assets/i18n/ru.yaml b/assets/i18n/ru.yaml index 27ba33ced..e3d2ac234 100644 --- a/assets/i18n/ru.yaml +++ b/assets/i18n/ru.yaml @@ -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": "Общие" diff --git a/assets/i18n/zh-CN.yaml b/assets/i18n/zh-CN.yaml index 3b838ad43..c2fb45995 100644 --- a/assets/i18n/zh-CN.yaml +++ b/assets/i18n/zh-CN.yaml @@ -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": "常规" diff --git a/crates/oak-app/src/app.rs b/crates/oak-app/src/app.rs index bc57ec546..fdc2096ea 100644 --- a/crates/oak-app/src/app.rs +++ b/crates/oak-app/src/app.rs @@ -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 { modal: Entity, content: Entity, }, + /// The new-project dialog (文件 → 新建项目): name + sequence format. + NewProject { + modal: Entity, + content: Entity, + }, } /// A running export: the session the tick loop drains for progress. @@ -275,6 +282,7 @@ impl ModalState { | 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 OakApp { 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 OakApp { /// 新建项目: 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) { - 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) { + 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, cx: &mut Context) { + 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 OakApp { } } } + 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" ); } diff --git a/crates/oak-app/src/dialogs.rs b/crates/oak-app/src/dialogs.rs index e769fc913..c0ecb63ac 100644 --- a/crates/oak-app/src/dialogs.rs +++ b/crates/oak-app/src/dialogs.rs @@ -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, + format: Entity, +} + +impl NewProjectContent { + /// Builds the dialog (default name, HD 1080p25). + pub fn new(window: &mut Window, cx: &mut Context) -> 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) -> 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::*;