use super::*; impl BdsApp { pub(super) fn handle_post_editor_msg(&mut self, msg: PostEditorMsg) -> Task { enum DeferredPostAction { None, SyncEmbeddedPreview, Analyze(String), AnalyzeTaxonomy(String), EnsureTag { post_id: String, tag: String, }, LoadSemanticTags(String), AddGalleryImages(String), DetectLanguage(String), OpenTranslate(String), TranslateTo { post_id: String, target_language: String, }, Save(String), Publish(String), Discard(String), ShowDelete { tab_id: String, name: String, }, OpenInsertLink(String), OpenInsertMedia { post_id: String, link_only: bool, }, OpenGallery(String), OpenLinkedMedia(String), UnlinkLinkedMedia { post_id: String, media_id: String, }, InsertSelectedLink { post_id: String, linked_post_id: String, }, CreateLinkedPost(String), InsertSelectedMedia { post_id: String, media_id: String, }, SetLinkTab(modal::PostInsertLinkTab), SetLinkSearch(String), SetExternalUrl(String), SetExternalText(String), InsertExternalLink, SetMediaSearch(String), SelectGalleryImage(usize), GalleryPrevious, GalleryNext, GalleryCloseLightbox, } let mut deferred = DeferredPostAction::None; let mut refresh_linked_media: Option<(String, String)> = None; if let Some(tab_id) = self.active_tab.clone() && let Some(state) = self.post_editors.get_mut(&tab_id) { match msg { PostEditorMsg::ToggleQuickActions => { state.quick_actions_open = !state.quick_actions_open; } PostEditorMsg::AnalyzeWithAi => { state.quick_actions_open = false; deferred = DeferredPostAction::Analyze(tab_id.clone()); } PostEditorMsg::AnalyzeTaxonomy => { state.quick_actions_open = false; deferred = DeferredPostAction::AnalyzeTaxonomy(tab_id.clone()); } PostEditorMsg::AddGalleryImages => { state.quick_actions_open = false; deferred = DeferredPostAction::AddGalleryImages(state.post_id.clone()); } PostEditorMsg::SwitchEditorMode(mode) => { state.set_editor_mode(&mode); deferred = DeferredPostAction::SyncEmbeddedPreview; } PostEditorMsg::DetectLanguage => { state.quick_actions_open = false; deferred = DeferredPostAction::DetectLanguage(tab_id.clone()); } PostEditorMsg::Translate => { state.quick_actions_open = false; deferred = DeferredPostAction::OpenTranslate(tab_id.clone()); } PostEditorMsg::TranslateTo(target_language) => { deferred = DeferredPostAction::TranslateTo { post_id: tab_id.clone(), target_language, }; } PostEditorMsg::TitleChanged(s) => { state.title = s; state.mark_dirty(); } PostEditorMsg::SlugChanged(s) => { state.slug = s; state.mark_dirty(); } PostEditorMsg::ExcerptChanged(s) => { state.excerpt = s; state.mark_dirty(); } PostEditorMsg::ContentChanged(new_text) => { state.content = new_text; state.mark_dirty(); refresh_linked_media = Some((state.post_id.clone(), state.content.clone())); } PostEditorMsg::AuthorChanged(s) => { state.author = s; state.mark_dirty(); } PostEditorMsg::LanguageChanged(s) => { state.language = s; state.mark_dirty(); } PostEditorMsg::TemplateSlugChanged(s) => { state.template_slug = s; state.mark_dirty(); } PostEditorMsg::ToggleDoNotTranslate(b) => { state.do_not_translate = b; state.mark_dirty(); } PostEditorMsg::ToggleMetadata => { state.metadata_expanded = !state.metadata_expanded; if state.metadata_expanded { deferred = DeferredPostAction::LoadSemanticTags(state.post_id.clone()); } } PostEditorMsg::ToggleExcerpt => { state.excerpt_expanded = !state.excerpt_expanded; } PostEditorMsg::SwitchLanguage(lang) => { state.switch_language(&lang); if state.editor_mode == "preview" { deferred = DeferredPostAction::SyncEmbeddedPreview; } } PostEditorMsg::TagsInputChanged(s) => { state.tags_input = s; if state.tags_input.trim().is_empty() { deferred = DeferredPostAction::LoadSemanticTags(state.post_id.clone()); } } PostEditorMsg::TagsInputSubmit => { let tag = state.tags_input.trim().to_string(); if !tag.is_empty() && !state .tags .iter() .any(|current| current.eq_ignore_ascii_case(&tag)) { state.tags.push(tag.clone()); state.mark_dirty(); deferred = DeferredPostAction::EnsureTag { post_id: state.post_id.clone(), tag, }; } state.tags_input.clear(); } PostEditorMsg::AddSuggestedTag(tag) => { if !state .tags .iter() .any(|current| current.eq_ignore_ascii_case(&tag)) { state.tags.push(tag.clone()); state.mark_dirty(); deferred = DeferredPostAction::EnsureTag { post_id: state.post_id.clone(), tag: tag.clone(), }; } state.tags_input.clear(); } PostEditorMsg::RemoveTag(tag) => { state.tags.retain(|t| t != &tag); state.mark_dirty(); } PostEditorMsg::CategoriesInputChanged(s) => { state.categories_input = s; } PostEditorMsg::CategoriesInputSubmit => { let cat = state.categories_input.trim().to_string(); if !cat.is_empty() && !state.categories.contains(&cat) { state.categories.push(cat); state.mark_dirty(); } state.categories_input.clear(); } PostEditorMsg::RemoveCategory(cat) => { state.categories.retain(|c| c != &cat); state.mark_dirty(); } PostEditorMsg::Save => { deferred = DeferredPostAction::Save(tab_id.clone()); } PostEditorMsg::Publish => { deferred = DeferredPostAction::Publish(tab_id.clone()); } PostEditorMsg::Discard => { deferred = DeferredPostAction::Discard(tab_id.clone()); } PostEditorMsg::Delete => { deferred = DeferredPostAction::ShowDelete { tab_id: tab_id.clone(), name: state.title.clone(), }; } PostEditorMsg::InsertLink => { deferred = DeferredPostAction::OpenInsertLink(state.post_id.clone()); } PostEditorMsg::InsertMedia => { deferred = DeferredPostAction::OpenInsertMedia { post_id: state.post_id.clone(), link_only: false, }; } PostEditorMsg::Gallery => { deferred = DeferredPostAction::OpenGallery(state.post_id.clone()); } PostEditorMsg::LinkExistingMedia => { deferred = DeferredPostAction::OpenInsertMedia { post_id: state.post_id.clone(), link_only: true, }; } PostEditorMsg::OpenLinkedMedia(media_id) => { deferred = DeferredPostAction::OpenLinkedMedia(media_id); } PostEditorMsg::UnlinkLinkedMedia(media_id) => { deferred = DeferredPostAction::UnlinkLinkedMedia { post_id: state.post_id.clone(), media_id, }; } PostEditorMsg::PostInsertLinkSelected(linked_post_id) => { deferred = DeferredPostAction::InsertSelectedLink { post_id: state.post_id.clone(), linked_post_id, }; } PostEditorMsg::PostInsertLinkCreate => { deferred = DeferredPostAction::CreateLinkedPost(state.post_id.clone()); } PostEditorMsg::PostInsertMediaSelected(media_id) => { deferred = DeferredPostAction::InsertSelectedMedia { post_id: state.post_id.clone(), media_id, }; } PostEditorMsg::PostGalleryImageSelected(index) => { deferred = DeferredPostAction::SelectGalleryImage(index); } PostEditorMsg::PostInsertLinkTabSwitch(tab) => { deferred = DeferredPostAction::SetLinkTab(tab); } PostEditorMsg::PostInsertLinkSearch(query) => { deferred = DeferredPostAction::SetLinkSearch(query); } PostEditorMsg::PostInsertLinkUrlChanged(url) => { deferred = DeferredPostAction::SetExternalUrl(url); } PostEditorMsg::PostInsertLinkTextChanged(text) => { deferred = DeferredPostAction::SetExternalText(text); } PostEditorMsg::PostInsertLinkExternalInsert => { deferred = DeferredPostAction::InsertExternalLink; } PostEditorMsg::PostInsertMediaSearch(query) => { deferred = DeferredPostAction::SetMediaSearch(query); } PostEditorMsg::PostGalleryPrevious => { deferred = DeferredPostAction::GalleryPrevious; } PostEditorMsg::PostGalleryNext => { deferred = DeferredPostAction::GalleryNext; } PostEditorMsg::PostGalleryCloseLightbox => { deferred = DeferredPostAction::GalleryCloseLightbox; } } if let Some(tab) = self.tabs.iter_mut().find(|t| t.id == state.post_id) { tab.is_dirty = state.is_dirty; } } if let Some((post_id, content)) = refresh_linked_media { let linked_media = self.load_post_media_items(&post_id, Some(&content)); if let Some(state) = self.post_editors.get_mut(&post_id) { state.linked_media = linked_media; } } match deferred { DeferredPostAction::None => Task::none(), DeferredPostAction::SyncEmbeddedPreview => self.sync_embedded_preview_for_active_post(), DeferredPostAction::Analyze(tab_id) => self.run_post_ai_analysis(&tab_id), DeferredPostAction::AnalyzeTaxonomy(tab_id) => self.run_post_taxonomy_analysis(&tab_id), DeferredPostAction::EnsureTag { post_id, tag } => { self.ensure_post_editor_tag(&post_id, &tag) } DeferredPostAction::LoadSemanticTags(post_id) => { Task::done(Message::LoadSemanticTagSuggestions(post_id)) } DeferredPostAction::AddGalleryImages(post_id) => self.add_gallery_images(&post_id), DeferredPostAction::DetectLanguage(tab_id) => self.detect_post_language(&tab_id), DeferredPostAction::OpenTranslate(tab_id) => self.open_post_translation_modal(&tab_id), DeferredPostAction::TranslateTo { post_id, target_language, } => self.translate_post_to(&post_id, &target_language), DeferredPostAction::Save(tab_id) => self.save_post_editor(&tab_id, true), DeferredPostAction::Publish(tab_id) => self.publish_post_editor(&tab_id), DeferredPostAction::Discard(tab_id) => self.discard_post_editor(&tab_id), DeferredPostAction::ShowDelete { tab_id, name } => { Task::done(Message::ShowModal(modal::ModalState::ConfirmDelete { entity_name: name, references: Vec::new(), on_confirm: modal::ConfirmAction::DeletePost(tab_id), })) } DeferredPostAction::OpenInsertLink(post_id) => self.insert_link_modal(&post_id), DeferredPostAction::OpenInsertMedia { post_id, link_only } => { self.insert_media_modal(&post_id, link_only) } DeferredPostAction::OpenGallery(post_id) => self.post_gallery(&post_id), DeferredPostAction::OpenLinkedMedia(media_id) => { let title = self .db .as_ref() .and_then(|db| { bds_core::db::queries::media::get_media_by_id(db.conn(), &media_id) .ok() .map(|media| media.title.unwrap_or(media.original_name)) }) .unwrap_or_else(|| media_id.clone()); Task::done(Message::OpenTab(Tab { id: media_id, tab_type: TabType::Media, title, is_transient: false, is_dirty: false, })) } DeferredPostAction::UnlinkLinkedMedia { post_id, media_id } => { if let (Some(db), Some(data_dir)) = (&self.db, &self.data_dir) { if let Err(err) = engine::post_media::unlink_media_from_post( db.conn(), data_dir, &post_id, &media_id, ) { self.notify_operation_failed("editor.unlinkMedia", err); return Task::none(); } self.refresh_post_relationships(&post_id); } Task::none() } DeferredPostAction::InsertSelectedLink { post_id, linked_post_id, } => self.insert_selected_post_link(&post_id, &linked_post_id), DeferredPostAction::CreateLinkedPost(post_id) => { self.insert_created_post_link(&post_id) } DeferredPostAction::InsertSelectedMedia { post_id, media_id } => { self.insert_selected_media(&post_id, &media_id) } DeferredPostAction::SetLinkTab(tab) => { self.refresh_post_insert_link_modal(Some(tab), None, None, None); Task::none() } DeferredPostAction::SetLinkSearch(query) => { self.refresh_post_insert_link_modal(None, Some(query), None, None); Task::none() } DeferredPostAction::SetExternalUrl(url) => { self.refresh_post_insert_link_modal(None, None, Some(url), None); Task::none() } DeferredPostAction::SetExternalText(text) => { self.refresh_post_insert_link_modal(None, None, None, Some(text)); Task::none() } DeferredPostAction::InsertExternalLink => { if let Some(modal::ModalState::PostInsertLink { post_id, external_url, external_text, .. }) = self.active_modal.clone() { if let Some(markdown) = modal::external_link_markdown(&external_url, &external_text) { self.insert_markdown_into_post(&post_id, &markdown) } else { self.notify( ToastLevel::Error, &t(self.ui_locale, "modal.postInsertLink.urlRequired"), ); Task::none() } } else { Task::none() } } DeferredPostAction::SetMediaSearch(query) => { self.refresh_insert_media_modal(query); Task::none() } DeferredPostAction::SelectGalleryImage(index) => { self.update_gallery_selection(Some(index)); Task::none() } DeferredPostAction::GalleryPrevious => { self.step_gallery_selection(-1); Task::none() } DeferredPostAction::GalleryNext => { self.step_gallery_selection(1); Task::none() } DeferredPostAction::GalleryCloseLightbox => { self.update_gallery_selection(None); Task::none() } } } pub(super) fn handle_media_editor_msg(&mut self, msg: MediaEditorMsg) -> Task { enum DeferredMediaAction { None, Analyze(String), DetectLanguage(String), OpenTranslate(String), TranslateTo { media_id: String, target_language: String, }, LinkPost { media_id: String, post_id: String, }, OpenLinkedPost(String), UnlinkPost { media_id: String, post_id: String, }, Save(String), Replace(String), Delete { tab_id: String, name: String, }, } let mut deferred = DeferredMediaAction::None; let mut picker_refresh: Option<(String, String)> = None; if let Some(tab_id) = self.active_tab.clone() && let Some(state) = self.media_editors.get_mut(&tab_id) { match msg { MediaEditorMsg::ToggleQuickActions => { state.quick_actions_open = !state.quick_actions_open; } MediaEditorMsg::AnalyzeWithAi => { state.quick_actions_open = false; deferred = DeferredMediaAction::Analyze(tab_id.clone()); } MediaEditorMsg::DetectLanguage => { state.quick_actions_open = false; deferred = DeferredMediaAction::DetectLanguage(tab_id.clone()); } MediaEditorMsg::TranslateMetadata => { state.quick_actions_open = false; deferred = DeferredMediaAction::OpenTranslate(tab_id.clone()); } MediaEditorMsg::TranslateTo(target_language) => { deferred = DeferredMediaAction::TranslateTo { media_id: tab_id.clone(), target_language, }; } MediaEditorMsg::TitleChanged(s) => { state.title = s; state.is_dirty = true; } MediaEditorMsg::AltChanged(s) => { state.alt = s; state.is_dirty = true; } MediaEditorMsg::CaptionChanged(s) => { state.caption = s; state.is_dirty = true; } MediaEditorMsg::AuthorChanged(s) => { state.author = s; state.is_dirty = true; } MediaEditorMsg::LanguageChanged(s) => { state.language = s; state.is_dirty = true; } MediaEditorMsg::TagsChanged(s) => { state.tags_input = s; state.is_dirty = true; } MediaEditorMsg::SwitchLanguage(lang) => { state.switch_language(&lang); } MediaEditorMsg::TogglePostPicker => { let next_open = !state.post_picker_open; state.post_picker_open = next_open; if !next_open { state.post_picker_results.clear(); } else { picker_refresh = Some((state.media_id.clone(), state.post_picker_search.clone())); } } MediaEditorMsg::PostPickerSearchChanged(search) => { state.post_picker_search = search; if state.post_picker_open { picker_refresh = Some((state.media_id.clone(), state.post_picker_search.clone())); } } MediaEditorMsg::LinkPost(post_id) => { deferred = DeferredMediaAction::LinkPost { media_id: state.media_id.clone(), post_id, }; } MediaEditorMsg::OpenLinkedPost(post_id) => { deferred = DeferredMediaAction::OpenLinkedPost(post_id); } MediaEditorMsg::UnlinkPost(post_id) => { deferred = DeferredMediaAction::UnlinkPost { media_id: state.media_id.clone(), post_id, }; } MediaEditorMsg::Save => { deferred = DeferredMediaAction::Save(tab_id.clone()); } MediaEditorMsg::ReplaceFile => { deferred = DeferredMediaAction::Replace(tab_id.clone()); } MediaEditorMsg::Delete => { deferred = DeferredMediaAction::Delete { tab_id: tab_id.clone(), name: state.title.clone(), }; } } if let Some(tab) = self .tabs .iter_mut() .find(|t| t.id == *state.media_id.as_str()) { tab.is_dirty = state.is_dirty; } } if let Some((media_id, search)) = picker_refresh { let results = self.query_media_post_picker_results(&media_id, &search); if let Some(state) = self.media_editors.get_mut(&media_id) { state.post_picker_results = results; } } match deferred { DeferredMediaAction::None => Task::none(), DeferredMediaAction::Analyze(media_id) => self.run_media_ai_analysis(&media_id), DeferredMediaAction::DetectLanguage(media_id) => self.detect_media_language(&media_id), DeferredMediaAction::OpenTranslate(media_id) => { self.open_media_translation_modal(&media_id) } DeferredMediaAction::TranslateTo { media_id, target_language, } => self.translate_media_to(&media_id, &target_language), DeferredMediaAction::LinkPost { media_id, post_id } => { self.link_media_to_post(&media_id, &post_id) } DeferredMediaAction::OpenLinkedPost(post_id) => { let title = self .db .as_ref() .and_then(|db| { bds_core::db::queries::post::get_post_by_id(db.conn(), &post_id).ok() }) .map(|post| post.title) .unwrap_or_else(|| post_id.clone()); Task::done(Message::OpenTab(Tab { id: post_id, tab_type: TabType::Post, title, is_transient: false, is_dirty: false, })) } DeferredMediaAction::UnlinkPost { media_id, post_id } => { self.unlink_media_from_post(&media_id, &post_id) } DeferredMediaAction::Save(tab_id) => self.save_media_editor(&tab_id), DeferredMediaAction::Replace(media_id) => { crate::platform::dialog::pick_media_replacement( media_id, t(self.ui_locale, "dialog.replaceMedia"), t(self.ui_locale, "dialog.imageFilter"), ) } DeferredMediaAction::Delete { tab_id, name } => { Task::done(Message::ShowModal(modal::ModalState::ConfirmDelete { entity_name: name, references: Vec::new(), on_confirm: modal::ConfirmAction::DeleteMedia(tab_id), })) } } } pub(super) fn handle_template_editor_msg(&mut self, msg: TemplateEditorMsg) -> Task { if let Some(tab_id) = self.active_tab.clone() && let Some(state) = self.template_editors.get_mut(&tab_id) { match msg { TemplateEditorMsg::TitleChanged(s) => { state.title = s; state.is_dirty = true; } TemplateEditorMsg::SlugChanged(s) => { state.slug = s; state.is_dirty = true; } TemplateEditorMsg::KindChanged(k) => { state.kind = k.0; state.is_dirty = true; } TemplateEditorMsg::EnabledChanged(b) => { state.enabled = b; state.is_dirty = true; } TemplateEditorMsg::ContentChanged(new_text) => { state.content = new_text; state.is_dirty = true; } TemplateEditorMsg::Save => { return self.save_template_editor(&tab_id); } TemplateEditorMsg::Validate => { if let Some(st) = self.template_editors.get_mut(&tab_id) { match engine::template::validate_template(&st.content) { Ok(()) => { st.validation_error = None; } Err(e) => { st.validation_error = Some(e); } } } } TemplateEditorMsg::Delete => { return self.show_template_delete_confirmation(&tab_id); } } if let Some(st) = self.template_editors.get(&tab_id) && let Some(tab) = self.tabs.iter_mut().find(|t| t.id == tab_id) { tab.is_dirty = st.is_dirty; } } Task::none() } pub(super) fn handle_script_editor_msg(&mut self, msg: ScriptEditorMsg) -> Task { let mut run_request = None; let mut syntax_notification = None; if let Some(tab_id) = self.active_tab.clone() && let Some(state) = self.script_editors.get_mut(&tab_id) { match msg { ScriptEditorMsg::TitleChanged(s) => { state.title = s; state.is_dirty = true; } ScriptEditorMsg::SlugChanged(s) => { state.slug = s; state.is_dirty = true; } ScriptEditorMsg::KindChanged(k) => { state.kind = k.0; state.is_dirty = true; } ScriptEditorMsg::EntrypointChanged(s) => { state.entrypoint = s; state.is_dirty = true; } ScriptEditorMsg::EnabledChanged(b) => { state.enabled = b; state.is_dirty = true; } ScriptEditorMsg::ContentChanged(new_text) => { state.discovered_entrypoints = engine::script::discover_entrypoints(&new_text); state.content = new_text; state.is_dirty = true; } ScriptEditorMsg::Save => { return self.save_script_editor(&tab_id); } ScriptEditorMsg::CheckSyntax => { match engine::script::validate_script_syntax(&state.content) { Ok(()) => { state.validation_error = None; syntax_notification = Some(( ToastLevel::Success, t(self.ui_locale, "editor.syntaxValid"), )); } Err(error) => { state.validation_error = Some(error.clone()); syntax_notification = Some(( ToastLevel::Error, tw(self.ui_locale, "editor.syntaxInvalid", &[("error", &error)]), )); } } } ScriptEditorMsg::Run => { run_request = Some(( state.content.clone(), state.entrypoint.clone(), state.kind.clone(), )); } ScriptEditorMsg::Delete => { let name = state.title.clone(); return Task::done(Message::ShowModal(modal::ModalState::ConfirmDelete { entity_name: name, references: Vec::new(), on_confirm: modal::ConfirmAction::DeleteScript(tab_id), })); } } if let Some(st) = self.script_editors.get(&tab_id) && let Some(tab) = self.tabs.iter_mut().find(|t| t.id == tab_id) { tab.is_dirty = st.is_dirty; } } if let Some((level, message)) = syntax_notification { self.notify(level, &message); } if let Some((source, entrypoint, kind)) = run_request { let offline_mode = self.offline_mode; let app_handler = crate::platform::script_host::handler( Arc::clone(&self.script_menu_actions), t(self.ui_locale, "dialog.selectFolder"), ); return self.spawn_engine_task( "engine.runScript", move |db_path, project_id, data_dir, task_manager, task_id| { let execution_kind = match kind { bds_core::model::ScriptKind::Macro => { bds_core::scripting::ExecutionKind::Macro } bds_core::model::ScriptKind::Utility => { bds_core::scripting::ExecutionKind::Utility } bds_core::model::ScriptKind::Transform => { bds_core::scripting::ExecutionKind::Transform } }; let host = bds_core::scripting::CoreHost::new(db_path, project_id, data_dir) .with_task(Arc::clone(&task_manager), task_id) .with_offline_mode(offline_mode) .with_app_handler(Arc::clone(&app_handler)); let control = task_manager .cancellation_flag(task_id) .map(bds_core::scripting::ExecutionControl::from_cancelled) .unwrap_or_default(); let result = bds_core::scripting::execute_with_host( &source, &entrypoint, &serde_json::json!({}), execution_kind, &control, Arc::new(host), )?; let mut lines = result.output; lines.extend( result .toasts .into_iter() .map(|message| format!("toast: {message}")), ); if result.value != serde_json::Value::Null { lines.push(result.value.to_string()); } Ok(if lines.is_empty() { "completed".to_string() } else { lines.join("\n") }) }, ); } Task::none() } }