feat: even more M4 closing

This commit is contained in:
2026-04-13 20:45:29 +02:00
parent 316a218685
commit eca5fafc1c
12 changed files with 521 additions and 9 deletions

View File

@@ -137,8 +137,10 @@ pub enum Message {
GenerateSite,
RunMetadataDiff,
RunSiteValidation,
ApplySiteValidation,
EngineTaskDone { task_id: TaskId, label: String, result: Result<String, String> },
SiteValidationLoaded(Result<engine::validate_site::SiteValidationReport, String>),
SiteValidationApplied(Result<String, String>),
// Editor views
PostEditor(PostEditorMsg),
@@ -2351,6 +2353,7 @@ impl BdsApp {
Task::none()
}
Message::RunSiteValidation => self.start_site_validation(),
Message::ApplySiteValidation => self.apply_site_validation(),
Message::EngineTaskDone { task_id, label, result } => {
match &result {
Ok(detail) => {
@@ -2396,6 +2399,21 @@ impl BdsApp {
}
Task::none()
}
Message::SiteValidationApplied(result) => {
self.site_validation_state.is_applying = false;
match result {
Ok(detail) => {
self.site_validation_state.error_message = None;
self.notify(ToastLevel::Success, &detail);
self.start_site_validation()
}
Err(error) => {
self.site_validation_state.error_message = Some(error.clone());
self.notify(ToastLevel::Error, &error);
Task::none()
}
}
}
// ── Toast ──
Message::ShowToast(level, msg) => {
@@ -3703,6 +3721,70 @@ impl BdsApp {
)
}
fn apply_site_validation(&mut self) -> Task<Message> {
if self.site_validation_state.is_running || self.site_validation_state.is_applying {
return Task::none();
}
let report = engine::validate_site::SiteValidationReport {
missing_pages: self.site_validation_state.missing_files.clone(),
extra_pages: self.site_validation_state.extra_files.clone(),
stale_pages: self.site_validation_state.stale_files.clone(),
};
let sections = engine::generation::sections_from_validation_report(&report);
if sections.is_empty() {
return Task::none();
}
let Some(project_id) = self.active_project.as_ref().map(|project| project.id.clone()) else {
self.site_validation_state.error_message = Some(t(self.ui_locale, "engine.generateSiteNoProject"));
return Task::none();
};
let Some(data_dir) = self.data_dir.clone() else {
self.site_validation_state.error_message = Some(t(self.ui_locale, "engine.previewDataDirUnavailable"));
return Task::none();
};
self.site_validation_state.is_applying = true;
self.site_validation_state.error_message = None;
let db_path = self.db_path.clone();
let applied_label = t(self.ui_locale, "siteValidation.apply");
Task::perform(
async move {
let db = Database::open(&db_path).map_err(|error| error.to_string())?;
let metadata = engine::meta::read_project_json(&data_dir).map_err(|error| error.to_string())?;
let all_posts = bds_core::db::queries::post::list_posts_by_project(db.conn(), &project_id)
.map_err(|error| error.to_string())?;
let mut sources = Vec::new();
for post in all_posts.into_iter().filter(|post| post.status == PostStatus::Published) {
let body_markdown = load_generation_post_body(&data_dir, &post)?;
sources.push(engine::generation::PublishedPostSource { post, body_markdown });
}
let output_dir = data_dir.join("html");
std::fs::create_dir_all(&output_dir).map_err(|error| error.to_string())?;
let apply_report = engine::generation::apply_validation_sections(
db.conn(),
&output_dir,
&project_id,
&metadata,
&sources,
&sections,
)
.map_err(|error| error.to_string())?;
Ok(format!(
"{}: written={}, skipped={}, deleted={}, output={}",
applied_label,
apply_report.written_paths.len(),
apply_report.skipped_paths.len(),
apply_report.deleted_paths.len(),
output_dir.display(),
))
},
Message::SiteValidationApplied,
)
}
/// Spawn a blocking engine operation on a background thread via TaskManager.
///
/// Returns `Task::none()` if no active project/db/data_dir.

View File

@@ -11,6 +11,7 @@ use crate::i18n::t;
pub struct SiteValidationState {
pub has_run: bool,
pub is_running: bool,
pub is_applying: bool,
pub missing_files: Vec<String>,
pub extra_files: Vec<String>,
pub stale_files: Vec<String>,
@@ -24,6 +25,15 @@ pub fn view<'a>(state: &'a SiteValidationState, locale: UiLocale) -> Element<'a,
button(text(t(locale, "siteValidation.run")).size(13).shaping(Shaping::Advanced))
.on_press(Message::RunSiteValidation)
};
let has_issues = !state.missing_files.is_empty() || !state.extra_files.is_empty() || !state.stale_files.is_empty();
let apply_button = if state.is_applying {
button(text(t(locale, "siteValidation.applying")).size(13).shaping(Shaping::Advanced))
} else if !state.is_running && state.error_message.is_none() && has_issues {
button(text(t(locale, "siteValidation.apply")).size(13).shaping(Shaping::Advanced))
.on_press(Message::ApplySiteValidation)
} else {
button(text(t(locale, "siteValidation.apply")).size(13).shaping(Shaping::Advanced))
};
let mut content = column![
row![
@@ -31,7 +41,7 @@ pub fn view<'a>(state: &'a SiteValidationState, locale: UiLocale) -> Element<'a,
.size(24)
.shaping(Shaping::Advanced)
.color(Color::from_rgb(0.88, 0.88, 0.92)),
run_button,
row![run_button, apply_button].spacing(12),
]
.spacing(16),
]