fix: more work on background tasks
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -12,6 +12,7 @@ impl BdsApp {
|
||||
let on_progress: engine::rebuild::ProgressFn =
|
||||
Arc::new(move |pct, event| {
|
||||
tm.report_progress(tid, Some(pct), Some(event.localized(locale)));
|
||||
!tm.is_cancelled(tid)
|
||||
});
|
||||
let report = engine::rebuild::rebuild_from_filesystem_with_progress(
|
||||
db.conn(),
|
||||
@@ -42,14 +43,28 @@ impl BdsApp {
|
||||
"engine.calendarStarted",
|
||||
move |db_path, project_id, data_dir, tm, tid| {
|
||||
let db = Database::open(&db_path).map_err(|e| e.to_string())?;
|
||||
tm.report_progress(tid, Some(0.20), Some(t(locale, "engine.loadingPosts")));
|
||||
engine::calendar::regenerate_calendar(db.conn(), &data_dir, &project_id)
|
||||
.map_err(|e| e.to_string())?;
|
||||
tm.report_progress(
|
||||
tid,
|
||||
Some(0.90),
|
||||
Some(t(locale, "engine.writingCalendar")),
|
||||
);
|
||||
engine::calendar::regenerate_calendar_with_progress(
|
||||
db.conn(),
|
||||
&data_dir,
|
||||
&project_id,
|
||||
|current, total, name| {
|
||||
tm.report_progress(
|
||||
tid,
|
||||
Some(current as f32 / total.max(1) as f32),
|
||||
Some(tw(
|
||||
locale,
|
||||
"engine.checkingItem",
|
||||
&[
|
||||
("current", ¤t.to_string()),
|
||||
("total", &total.to_string()),
|
||||
("name", name),
|
||||
],
|
||||
)),
|
||||
);
|
||||
!tm.is_cancelled(tid)
|
||||
},
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok("done".to_string())
|
||||
},
|
||||
)
|
||||
@@ -59,24 +74,39 @@ impl BdsApp {
|
||||
TabType::TranslationValidation,
|
||||
"tabBar.translationValidation",
|
||||
);
|
||||
let (Some(project), Some(data_dir)) = (&self.active_project, &self.data_dir) else {
|
||||
if self.active_project.is_none() || self.data_dir.is_none() {
|
||||
return Task::none();
|
||||
};
|
||||
}
|
||||
self.translation_validation_state.is_running = true;
|
||||
self.translation_validation_state.error_message = None;
|
||||
let db_path = self.db_path.clone();
|
||||
let project_id = project.id.clone();
|
||||
let data_dir = data_dir.clone();
|
||||
Task::perform(
|
||||
async move {
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let db = Database::open(&db_path).map_err(|e| e.to_string())?;
|
||||
let meta = engine::meta::read_project_json(&data_dir)
|
||||
.map_err(|e| e.to_string())?;
|
||||
let main_lang = meta.main_language.as_deref().unwrap_or("en");
|
||||
let blog_langs = meta.blog_languages.clone();
|
||||
let on_item: engine::validate_translations::ItemProgressFn =
|
||||
Box::new(move |_current, _total, _name| {});
|
||||
let locale = self.ui_locale;
|
||||
self.spawn_result_task(
|
||||
"menu.item.validateTranslations",
|
||||
move |db_path, project_id, data_dir, tm, tid| {
|
||||
let db = Database::open(&db_path).map_err(|e| e.to_string())?;
|
||||
let meta = engine::meta::read_project_json(&data_dir)
|
||||
.map_err(|e| e.to_string())?;
|
||||
let main_lang = meta.main_language.as_deref().unwrap_or("en");
|
||||
let blog_langs = meta.blog_languages.clone();
|
||||
let progress_tm = Arc::clone(&tm);
|
||||
let on_item: engine::validate_translations::ItemProgressFn =
|
||||
Box::new(move |current, total, name| {
|
||||
progress_tm.report_progress(
|
||||
tid,
|
||||
Some(current as f32 / total.max(1) as f32),
|
||||
Some(tw(
|
||||
locale,
|
||||
"engine.checkingItem",
|
||||
&[
|
||||
("current", ¤t.to_string()),
|
||||
("total", &total.to_string()),
|
||||
("name", name),
|
||||
],
|
||||
)),
|
||||
);
|
||||
!progress_tm.is_cancelled(tid)
|
||||
});
|
||||
let report =
|
||||
engine::validate_translations::validate_translations_with_progress(
|
||||
db.conn(),
|
||||
&data_dir,
|
||||
@@ -85,16 +115,31 @@ impl BdsApp {
|
||||
main_lang,
|
||||
Some(on_item),
|
||||
)
|
||||
.map_err(|e| e.to_string())
|
||||
})
|
||||
.await
|
||||
.unwrap_or_else(|error| Err(format!("task panicked: {error}")))
|
||||
.map_err(|e| e.to_string())?;
|
||||
tm.report_progress(
|
||||
tid,
|
||||
Some(1.0),
|
||||
Some(tw(
|
||||
locale,
|
||||
"engine.validateTranslationsComplete",
|
||||
&[
|
||||
("dbIssues", &report.db_issues.len().to_string()),
|
||||
("fsIssues", &report.fs_issues.len().to_string()),
|
||||
],
|
||||
)),
|
||||
);
|
||||
Ok(report)
|
||||
},
|
||||
Message::TranslationValidationLoaded,
|
||||
|task_id, result| Message::TranslationValidationLoaded { task_id, result },
|
||||
)
|
||||
}
|
||||
Message::TranslationValidationLoaded(result) => {
|
||||
Message::TranslationValidationLoaded { task_id, result } => {
|
||||
let cancelled = self.finish_result_task(task_id, &result);
|
||||
self.refresh_task_snapshots();
|
||||
self.translation_validation_state.is_running = false;
|
||||
if cancelled {
|
||||
return Task::none();
|
||||
}
|
||||
match result {
|
||||
Ok(report) => {
|
||||
self.translation_validation_state.report = Some(report);
|
||||
@@ -149,8 +194,13 @@ impl BdsApp {
|
||||
self.open_singleton_tab(TabType::MetadataDiff, "tabBar.metadataDiff");
|
||||
self.start_metadata_diff()
|
||||
}
|
||||
Message::MetadataDiffLoaded(result) => {
|
||||
Message::MetadataDiffLoaded { task_id, result } => {
|
||||
let cancelled = self.finish_result_task(task_id, &result);
|
||||
self.refresh_task_snapshots();
|
||||
self.metadata_diff_state.is_running = false;
|
||||
if cancelled {
|
||||
return Task::none();
|
||||
}
|
||||
match result {
|
||||
Ok(report) => {
|
||||
self.metadata_diff_state.report = Some(report);
|
||||
@@ -170,36 +220,38 @@ impl BdsApp {
|
||||
else {
|
||||
return Task::none();
|
||||
};
|
||||
let (Some(project), Some(data_dir)) =
|
||||
(self.active_project.as_ref(), self.data_dir.as_ref())
|
||||
else {
|
||||
if self.active_project.is_none() || self.data_dir.is_none() {
|
||||
return Task::none();
|
||||
};
|
||||
}
|
||||
self.metadata_diff_state.is_repairing = true;
|
||||
let db_path = self.db_path.clone();
|
||||
let project_id = project.id.clone();
|
||||
let data_dir = data_dir.clone();
|
||||
Task::perform(
|
||||
async move {
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let db = Database::open(&db_path).map_err(|error| error.to_string())?;
|
||||
engine::metadata_diff::repair_metadata_diff_item(
|
||||
db.conn(),
|
||||
&data_dir,
|
||||
&project_id,
|
||||
direction,
|
||||
&item,
|
||||
)
|
||||
.map_err(|error| error.to_string())
|
||||
})
|
||||
.await
|
||||
.map_err(|error| error.to_string())?
|
||||
let locale = self.ui_locale;
|
||||
self.spawn_result_task(
|
||||
"metadataDiff.repair",
|
||||
move |db_path, project_id, data_dir, tm, tid| {
|
||||
tm.report_progress(tid, Some(0.0), Some(t(locale, "metadataDiff.repair")));
|
||||
if tm.is_cancelled(tid) {
|
||||
return Err("operation cancelled".into());
|
||||
}
|
||||
let db = Database::open(&db_path).map_err(|error| error.to_string())?;
|
||||
engine::metadata_diff::repair_metadata_diff_item(
|
||||
db.conn(),
|
||||
&data_dir,
|
||||
&project_id,
|
||||
direction,
|
||||
&item,
|
||||
)
|
||||
.map_err(|error| error.to_string())
|
||||
},
|
||||
Message::MetadataDiffItemRepaired,
|
||||
|task_id, result| Message::MetadataDiffItemRepaired { task_id, result },
|
||||
)
|
||||
}
|
||||
Message::MetadataDiffItemRepaired(result) => {
|
||||
Message::MetadataDiffItemRepaired { task_id, result } => {
|
||||
let cancelled = self.finish_result_task(task_id, &result);
|
||||
self.refresh_task_snapshots();
|
||||
self.metadata_diff_state.is_repairing = false;
|
||||
if cancelled {
|
||||
return Task::none();
|
||||
}
|
||||
match result {
|
||||
Ok(()) => {
|
||||
self.notify(
|
||||
@@ -214,6 +266,62 @@ impl BdsApp {
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::ImportMetadataOrphan(index) => {
|
||||
let Some(orphan) = self
|
||||
.metadata_diff_state
|
||||
.report
|
||||
.as_ref()
|
||||
.and_then(|report| report.orphans.get(index))
|
||||
.cloned()
|
||||
else {
|
||||
return Task::none();
|
||||
};
|
||||
self.metadata_diff_state.is_repairing = true;
|
||||
let locale = self.ui_locale;
|
||||
self.spawn_result_task(
|
||||
"metadataDiff.importOrphan",
|
||||
move |db_path, project_id, data_dir, tm, tid| {
|
||||
tm.report_progress(
|
||||
tid,
|
||||
Some(0.0),
|
||||
Some(t(locale, "metadataDiff.importOrphan")),
|
||||
);
|
||||
if tm.is_cancelled(tid) {
|
||||
return Err("operation cancelled".into());
|
||||
}
|
||||
let db = Database::open(&db_path).map_err(|error| error.to_string())?;
|
||||
engine::metadata_diff::import_orphan_file(
|
||||
db.conn(),
|
||||
&data_dir,
|
||||
&project_id,
|
||||
&orphan,
|
||||
)
|
||||
.map_err(|error| error.to_string())
|
||||
},
|
||||
|task_id, result| Message::MetadataOrphanImported { task_id, result },
|
||||
)
|
||||
}
|
||||
Message::MetadataOrphanImported { task_id, result } => {
|
||||
let cancelled = self.finish_result_task(task_id, &result);
|
||||
self.refresh_task_snapshots();
|
||||
self.metadata_diff_state.is_repairing = false;
|
||||
if cancelled {
|
||||
return Task::none();
|
||||
}
|
||||
match result {
|
||||
Ok(()) => {
|
||||
self.notify(
|
||||
ToastLevel::Success,
|
||||
&t(self.ui_locale, "metadataDiff.orphanImported"),
|
||||
);
|
||||
self.start_metadata_diff()
|
||||
}
|
||||
Err(error) => {
|
||||
self.notify(ToastLevel::Error, &error);
|
||||
Task::none()
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::RunSiteValidation => self.start_site_validation(),
|
||||
Message::ApplySiteValidation => self.apply_site_validation(),
|
||||
Message::EngineTaskDone {
|
||||
@@ -223,15 +331,14 @@ impl BdsApp {
|
||||
result,
|
||||
} => {
|
||||
let search_rebuild_finished = self.search_index_rebuild_task_id == Some(task_id);
|
||||
let cancelled = self.task_manager.status(task_id) == Some(TaskStatus::Cancelled);
|
||||
let refresh_semantic_tags = !cancelled
|
||||
let cancellation_requested = self.task_manager.is_cancelled(task_id);
|
||||
let refresh_semantic_tags = !cancellation_requested
|
||||
&& result.is_ok()
|
||||
&& matches!(
|
||||
operation,
|
||||
"embeddings.indexing" | "menu.item.rebuildEmbeddingIndex"
|
||||
);
|
||||
match &result {
|
||||
_ if cancelled => {}
|
||||
Ok(detail) => {
|
||||
self.task_manager.complete(task_id);
|
||||
if operation == "engine.rebuildStarted" {
|
||||
@@ -258,12 +365,14 @@ impl BdsApp {
|
||||
}
|
||||
Err(err) => {
|
||||
self.task_manager.fail(task_id, err.clone());
|
||||
let message = tw(
|
||||
self.ui_locale,
|
||||
"common.operationFailed",
|
||||
&[("operation", &label), ("error", err)],
|
||||
);
|
||||
self.notify(ToastLevel::Error, &message);
|
||||
if !cancellation_requested {
|
||||
let message = tw(
|
||||
self.ui_locale,
|
||||
"common.operationFailed",
|
||||
&[("operation", &label), ("error", err)],
|
||||
);
|
||||
self.notify(ToastLevel::Error, &message);
|
||||
}
|
||||
}
|
||||
}
|
||||
if search_rebuild_finished {
|
||||
@@ -293,12 +402,41 @@ impl BdsApp {
|
||||
self.refresh_task_snapshots();
|
||||
Task::batch([sidebar_task, semantic_task])
|
||||
}
|
||||
Message::SiteGenerationPrepared {
|
||||
task_id,
|
||||
validation,
|
||||
force,
|
||||
result,
|
||||
} => {
|
||||
let cancelled = self.finish_result_task(task_id, &result);
|
||||
self.refresh_task_snapshots();
|
||||
if cancelled {
|
||||
if validation.is_some() {
|
||||
self.site_validation_state.is_applying = false;
|
||||
}
|
||||
return Task::none();
|
||||
}
|
||||
match result {
|
||||
Ok((prepared, counts)) => {
|
||||
self.queue_prepared_site_generation(validation, force, prepared, counts)
|
||||
}
|
||||
Err(error) => {
|
||||
if validation.is_some() {
|
||||
self.site_validation_state.is_applying = false;
|
||||
self.site_validation_state.error_message = Some(error.clone());
|
||||
}
|
||||
self.notify(ToastLevel::Error, &error);
|
||||
Task::none()
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::SiteGenerationSectionDone {
|
||||
group_id,
|
||||
task_id,
|
||||
result,
|
||||
} => {
|
||||
if self.task_manager.status(task_id) == Some(TaskStatus::Cancelled) {
|
||||
if self.task_manager.is_cancelled(task_id) {
|
||||
self.finish_result_task(task_id, &result);
|
||||
self.refresh_task_snapshots();
|
||||
return Task::none();
|
||||
}
|
||||
@@ -335,21 +473,54 @@ impl BdsApp {
|
||||
}
|
||||
}
|
||||
|
||||
let should_index =
|
||||
let next_phase =
|
||||
self.site_generation_workflows
|
||||
.get(&group_id)
|
||||
.is_some_and(|workflow| {
|
||||
workflow.index_task_id.is_none()
|
||||
.and_then(|workflow| {
|
||||
(workflow.calendar_task_id.is_none()
|
||||
&& workflow.index_task_id.is_none()
|
||||
&& workflow.render_task_ids.iter().all(|task_id| {
|
||||
self.task_manager.status(*task_id)
|
||||
== Some(TaskStatus::Completed)
|
||||
})
|
||||
}))
|
||||
.then_some(workflow.calendar_needed)
|
||||
});
|
||||
self.refresh_task_snapshots();
|
||||
if should_index {
|
||||
self.queue_site_search_index(&group_id)
|
||||
} else {
|
||||
Task::none()
|
||||
match next_phase {
|
||||
Some(true) => self.queue_site_calendar(&group_id),
|
||||
Some(false) => self.queue_site_search_index(&group_id),
|
||||
None => Task::none(),
|
||||
}
|
||||
}
|
||||
Message::SiteGenerationCalendarDone {
|
||||
group_id,
|
||||
task_id,
|
||||
result,
|
||||
} => {
|
||||
if self.task_manager.is_cancelled(task_id) {
|
||||
self.finish_result_task(task_id, &result);
|
||||
self.refresh_task_snapshots();
|
||||
return Task::none();
|
||||
}
|
||||
match result {
|
||||
Ok(()) => {
|
||||
self.task_manager.complete(task_id);
|
||||
self.refresh_task_snapshots();
|
||||
self.queue_site_search_index(&group_id)
|
||||
}
|
||||
Err(error) => {
|
||||
self.task_manager.fail(task_id, error.clone());
|
||||
self.task_manager.cancel_group(&group_id);
|
||||
if let Some(workflow) = self.site_generation_workflows.remove(&group_id)
|
||||
&& workflow.kind == SiteGenerationKind::Validation
|
||||
{
|
||||
self.site_validation_state.is_applying = false;
|
||||
self.site_validation_state.error_message = Some(error.clone());
|
||||
}
|
||||
self.notify(ToastLevel::Error, &error);
|
||||
self.refresh_task_snapshots();
|
||||
Task::none()
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::SiteGenerationIndexDone {
|
||||
@@ -357,7 +528,8 @@ impl BdsApp {
|
||||
task_id,
|
||||
result,
|
||||
} => {
|
||||
if self.task_manager.status(task_id) == Some(TaskStatus::Cancelled) {
|
||||
if self.task_manager.is_cancelled(task_id) {
|
||||
self.finish_result_task(task_id, &result);
|
||||
self.refresh_task_snapshots();
|
||||
return Task::none();
|
||||
}
|
||||
@@ -415,8 +587,13 @@ impl BdsApp {
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::SiteValidationLoaded(result) => {
|
||||
Message::SiteValidationLoaded { task_id, result } => {
|
||||
let cancelled = self.finish_result_task(task_id, &result);
|
||||
self.refresh_task_snapshots();
|
||||
self.site_validation_state.is_running = false;
|
||||
if cancelled {
|
||||
return Task::none();
|
||||
}
|
||||
self.site_validation_state.has_run = true;
|
||||
match result {
|
||||
Ok(report) => {
|
||||
|
||||
@@ -259,10 +259,11 @@ impl BdsApp {
|
||||
|
||||
Task::perform(
|
||||
async move {
|
||||
let Some(worker) = task_manager.admit(task_id).await else {
|
||||
return Err("cancelled".to_string());
|
||||
};
|
||||
tokio::task::spawn_blocking(move || {
|
||||
if !task_manager.wait_until_runnable(task_id) {
|
||||
return Err("cancelled".to_string());
|
||||
}
|
||||
let _worker = worker;
|
||||
let engine = GitEngine::new(&data_dir);
|
||||
let old_head = (operation == GitOperation::Pull)
|
||||
.then(|| engine.head())
|
||||
|
||||
@@ -14,35 +14,40 @@ fn shorten_task_url(url: &str) -> String {
|
||||
}
|
||||
|
||||
impl BdsApp {
|
||||
pub(super) fn finish_result_task<T>(
|
||||
&self,
|
||||
task_id: TaskId,
|
||||
result: &Result<T, String>,
|
||||
) -> bool {
|
||||
let cancellation_requested = self.task_manager.is_cancelled(task_id);
|
||||
match result {
|
||||
Ok(_) => self.task_manager.complete(task_id),
|
||||
Err(error) => self.task_manager.fail(task_id, error.clone()),
|
||||
}
|
||||
cancellation_requested && result.is_err()
|
||||
}
|
||||
|
||||
pub(super) fn refresh_task_snapshots(&mut self) {
|
||||
self.task_snapshots = self
|
||||
.task_manager
|
||||
.snapshots()
|
||||
.into_iter()
|
||||
.map(|snapshot| {
|
||||
let status_str = match &snapshot.status {
|
||||
TaskStatus::Pending => t(self.ui_locale, "tasks.statusPending"),
|
||||
TaskStatus::Running => t(self.ui_locale, "tasks.statusRunning"),
|
||||
TaskStatus::Completed => t(self.ui_locale, "tasks.statusCompleted"),
|
||||
TaskStatus::Failed(error) => {
|
||||
tw(self.ui_locale, "tasks.statusFailed", &[("error", error)])
|
||||
}
|
||||
TaskStatus::Cancelled => t(self.ui_locale, "tasks.statusCancelled"),
|
||||
};
|
||||
TaskSnapshot {
|
||||
id: snapshot.id,
|
||||
label: snapshot.label,
|
||||
group_id: snapshot.group_id,
|
||||
group_name: snapshot.group_name,
|
||||
status: status_str,
|
||||
progress: snapshot.progress,
|
||||
message: snapshot.message,
|
||||
is_cancellable: matches!(
|
||||
snapshot.status,
|
||||
TaskStatus::Pending | TaskStatus::Running
|
||||
),
|
||||
}
|
||||
.map(|snapshot| TaskSnapshot {
|
||||
id: snapshot.id,
|
||||
source: crate::state::navigation::TaskSource::Local,
|
||||
label: snapshot.label,
|
||||
group_id: snapshot.group_id,
|
||||
group_name: snapshot.group_name,
|
||||
status: snapshot.status.clone(),
|
||||
progress: snapshot.progress,
|
||||
message: snapshot.message,
|
||||
cancellation_requested: snapshot.cancellation_requested,
|
||||
is_cancellable: matches!(
|
||||
snapshot.status,
|
||||
TaskStatus::Pending | TaskStatus::Running
|
||||
) && !snapshot.cancellation_requested,
|
||||
})
|
||||
.chain(self.remote_task_snapshots.iter().cloned())
|
||||
.collect();
|
||||
}
|
||||
|
||||
@@ -69,7 +74,7 @@ impl BdsApp {
|
||||
} else {
|
||||
SiteGenerationKind::Full
|
||||
};
|
||||
let (Some(project), Some(data_dir)) = (&self.active_project, &self.data_dir) else {
|
||||
let Some(data_dir) = &self.data_dir else {
|
||||
if kind == SiteGenerationKind::Validation {
|
||||
self.site_validation_state.is_applying = false;
|
||||
}
|
||||
@@ -79,6 +84,16 @@ impl BdsApp {
|
||||
);
|
||||
return Task::none();
|
||||
};
|
||||
if self.active_project.is_none() {
|
||||
if kind == SiteGenerationKind::Validation {
|
||||
self.site_validation_state.is_applying = false;
|
||||
}
|
||||
self.notify(
|
||||
ToastLevel::Error,
|
||||
&t(self.ui_locale, "engine.generateSiteNoProject"),
|
||||
);
|
||||
return Task::none();
|
||||
}
|
||||
let metadata = match engine::meta::read_project_json(data_dir) {
|
||||
Ok(metadata) => metadata,
|
||||
Err(error) => {
|
||||
@@ -107,37 +122,122 @@ impl BdsApp {
|
||||
return Task::none();
|
||||
}
|
||||
|
||||
let sections = validation.as_ref().map_or_else(
|
||||
|| engine::generation::GenerationSection::ALL.to_vec(),
|
||||
|validation| engine::generation::sections_from_validation_report(validation, &metadata),
|
||||
);
|
||||
if sections.is_empty() {
|
||||
return Task::none();
|
||||
}
|
||||
let task_validation = validation.clone();
|
||||
let locale = self.ui_locale;
|
||||
self.spawn_result_task(
|
||||
"engine.prepareGeneration",
|
||||
move |db_path, project_id, data_dir, tm, tid| {
|
||||
tm.report_progress(
|
||||
tid,
|
||||
Some(0.0),
|
||||
Some(t(locale, "engine.preparingGeneration")),
|
||||
);
|
||||
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())?;
|
||||
if tm.is_cancelled(tid) {
|
||||
return Err("operation cancelled".into());
|
||||
}
|
||||
let posts =
|
||||
bds_core::db::queries::post::list_posts_by_project(db.conn(), &project_id)
|
||||
.map_err(|error| error.to_string())?
|
||||
.into_iter()
|
||||
.filter(engine::generation::has_published_snapshot)
|
||||
.collect::<Vec<_>>();
|
||||
let total_posts = posts.len();
|
||||
let mut sources = Vec::with_capacity(total_posts);
|
||||
for (index, post) in posts.into_iter().enumerate() {
|
||||
tm.report_progress(
|
||||
tid,
|
||||
Some(0.05 + (index + 1) as f32 / total_posts.max(1) as f32 * 0.35),
|
||||
Some(t(locale, "engine.loadingPosts")),
|
||||
);
|
||||
if tm.is_cancelled(tid) {
|
||||
return Err("operation cancelled".into());
|
||||
}
|
||||
if let Some(source) =
|
||||
engine::generation::load_published_post_source(&data_dir, post)
|
||||
.map_err(|error| error.to_string())?
|
||||
{
|
||||
sources.push(source);
|
||||
}
|
||||
}
|
||||
tm.report_progress(
|
||||
tid,
|
||||
Some(0.45),
|
||||
Some(t(locale, "engine.preparingGeneration")),
|
||||
);
|
||||
let prepared = Arc::new(
|
||||
engine::generation::prepare_site_generation(
|
||||
db.conn(),
|
||||
&data_dir,
|
||||
&project_id,
|
||||
&metadata,
|
||||
&sources,
|
||||
)
|
||||
.map_err(|error| error.to_string())?,
|
||||
);
|
||||
if tm.is_cancelled(tid) {
|
||||
return Err("operation cancelled".into());
|
||||
}
|
||||
let sections = task_validation.as_ref().map_or_else(
|
||||
|| engine::generation::GenerationSection::ALL.to_vec(),
|
||||
|report| engine::generation::sections_from_validation_report(report, &metadata),
|
||||
);
|
||||
let counts = sections
|
||||
.into_iter()
|
||||
.map(|section| {
|
||||
(
|
||||
section,
|
||||
engine::generation::prepared_section_page_count(
|
||||
&prepared,
|
||||
task_validation.as_ref(),
|
||||
section,
|
||||
),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
tm.report_progress(tid, Some(1.0), Some(t(locale, "engine.generationPrepared")));
|
||||
Ok((prepared, counts))
|
||||
},
|
||||
move |task_id, result| Message::SiteGenerationPrepared {
|
||||
task_id,
|
||||
validation: validation.clone(),
|
||||
force,
|
||||
result,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn queue_prepared_site_generation(
|
||||
&mut self,
|
||||
validation: Option<engine::validate_site::SiteValidationReport>,
|
||||
force: bool,
|
||||
prepared: Arc<engine::generation::PreparedSiteGeneration>,
|
||||
page_estimates: HashMap<engine::generation::GenerationSection, usize>,
|
||||
) -> Task<Message> {
|
||||
let kind = if validation.is_some() {
|
||||
SiteGenerationKind::Validation
|
||||
} else if force {
|
||||
SiteGenerationKind::Forced
|
||||
} else {
|
||||
SiteGenerationKind::Full
|
||||
};
|
||||
let (Some(project), Some(data_dir)) = (&self.active_project, &self.data_dir) else {
|
||||
return Task::none();
|
||||
};
|
||||
let sections = page_estimates.keys().copied().collect::<Vec<_>>();
|
||||
let calendar_needed = kind == SiteGenerationKind::Validation
|
||||
&& sections.iter().any(|section| {
|
||||
matches!(
|
||||
section,
|
||||
engine::generation::GenerationSection::Core
|
||||
| engine::generation::GenerationSection::Date
|
||||
)
|
||||
});
|
||||
let project_id = project.id.clone();
|
||||
let db_path = self.db_path.clone();
|
||||
let data_dir = data_dir.clone();
|
||||
let published_posts = match self.db.as_ref().and_then(|db| {
|
||||
bds_core::db::queries::post::list_posts_by_project(db.conn(), &project_id).ok()
|
||||
}) {
|
||||
Some(posts) => posts
|
||||
.into_iter()
|
||||
.filter(engine::generation::has_published_snapshot)
|
||||
.collect::<Vec<_>>(),
|
||||
None => return Task::none(),
|
||||
};
|
||||
let page_estimates = match bds_core::render::estimate_site_render_pages(
|
||||
&data_dir,
|
||||
&metadata,
|
||||
&published_posts,
|
||||
) {
|
||||
Ok(estimates) => estimates,
|
||||
Err(error) => {
|
||||
self.notify(ToastLevel::Error, &error.to_string());
|
||||
return Task::none();
|
||||
}
|
||||
};
|
||||
let group_id = format!("site-generation:{}", Uuid::new_v4());
|
||||
let group_name = t(
|
||||
self.ui_locale,
|
||||
@@ -150,7 +250,10 @@ impl BdsApp {
|
||||
let mut render_task_ids = Vec::new();
|
||||
let mut tasks = Vec::new();
|
||||
|
||||
for section in sections {
|
||||
for section in engine::generation::GenerationSection::ALL
|
||||
.into_iter()
|
||||
.filter(|section| sections.contains(section))
|
||||
{
|
||||
let label = t(self.ui_locale, generation_section_label_key(section));
|
||||
self.add_output(&label);
|
||||
let page_work = page_estimates[§ion];
|
||||
@@ -173,14 +276,20 @@ impl BdsApp {
|
||||
let task_data_dir = data_dir.clone();
|
||||
let task_group_id = group_id.clone();
|
||||
let task_validation = validation.clone();
|
||||
let task_prepared = Arc::clone(&prepared);
|
||||
let locale = self.ui_locale;
|
||||
tasks.push(Task::perform(
|
||||
async move {
|
||||
let Some(worker) = task_manager.admit(task_id).await else {
|
||||
return Err("cancelled".to_string());
|
||||
};
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let _worker = worker;
|
||||
run_site_generation_section(
|
||||
task_db_path,
|
||||
task_project_id,
|
||||
task_data_dir,
|
||||
task_prepared,
|
||||
task_manager,
|
||||
task_id,
|
||||
section,
|
||||
@@ -210,6 +319,8 @@ impl BdsApp {
|
||||
data_dir,
|
||||
group_name,
|
||||
render_task_ids,
|
||||
calendar_needed,
|
||||
calendar_task_id: None,
|
||||
index_task_id: None,
|
||||
report: engine::generation::GenerationReport::default(),
|
||||
},
|
||||
@@ -218,6 +329,75 @@ impl BdsApp {
|
||||
Task::batch(tasks)
|
||||
}
|
||||
|
||||
pub(super) fn queue_site_calendar(&mut self, group_id: &str) -> Task<Message> {
|
||||
let Some(workflow) = self.site_generation_workflows.get(group_id).cloned() else {
|
||||
return Task::none();
|
||||
};
|
||||
let label = t(self.ui_locale, "engine.calendarStarted");
|
||||
let task_id = self
|
||||
.task_manager
|
||||
.submit_grouped(&label, group_id, &workflow.group_name);
|
||||
if let Some(workflow) = self.site_generation_workflows.get_mut(group_id) {
|
||||
workflow.calendar_task_id = Some(task_id);
|
||||
}
|
||||
self.refresh_task_snapshots();
|
||||
let task_manager = Arc::clone(&self.task_manager);
|
||||
let task_group_id = group_id.to_string();
|
||||
let locale = self.ui_locale;
|
||||
Task::perform(
|
||||
async move {
|
||||
let Some(worker) = task_manager.admit(task_id).await else {
|
||||
return Err("cancelled".to_string());
|
||||
};
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let _worker = worker;
|
||||
task_manager.report_progress(
|
||||
task_id,
|
||||
Some(0.0),
|
||||
Some(t(locale, "engine.loadingPosts")),
|
||||
);
|
||||
let db =
|
||||
Database::open(&workflow.db_path).map_err(|error| error.to_string())?;
|
||||
engine::calendar::regenerate_calendar_with_progress(
|
||||
db.conn(),
|
||||
&workflow.data_dir,
|
||||
&workflow.project_id,
|
||||
|current, total, name| {
|
||||
task_manager.report_progress(
|
||||
task_id,
|
||||
Some(current as f32 / total.max(1) as f32),
|
||||
Some(tw(
|
||||
locale,
|
||||
"engine.checkingItem",
|
||||
&[
|
||||
("current", ¤t.to_string()),
|
||||
("total", &total.to_string()),
|
||||
("name", name),
|
||||
],
|
||||
)),
|
||||
);
|
||||
!task_manager.is_cancelled(task_id)
|
||||
},
|
||||
)
|
||||
.map_err(|error| error.to_string())?;
|
||||
task_manager.report_progress(
|
||||
task_id,
|
||||
Some(1.0),
|
||||
Some(t(locale, "engine.writingCalendar")),
|
||||
);
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.unwrap_or_else(|error| Err(format!("task panicked: {error}")))
|
||||
},
|
||||
move |result| Message::SiteGenerationCalendarDone {
|
||||
group_id: task_group_id.clone(),
|
||||
task_id,
|
||||
result,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn queue_site_search_index(&mut self, group_id: &str) -> Task<Message> {
|
||||
let Some(workflow) = self.site_generation_workflows.get(group_id).cloned() else {
|
||||
return Task::none();
|
||||
@@ -237,10 +417,11 @@ impl BdsApp {
|
||||
let task_group_id = group_id.to_string();
|
||||
Task::perform(
|
||||
async move {
|
||||
let Some(worker) = task_manager.admit(task_id).await else {
|
||||
return Err("cancelled".to_string());
|
||||
};
|
||||
tokio::task::spawn_blocking(move || {
|
||||
if !task_manager.wait_until_runnable(task_id) {
|
||||
return Err("cancelled".to_string());
|
||||
}
|
||||
let _worker = worker;
|
||||
let db =
|
||||
Database::open(&workflow.db_path).map_err(|error| error.to_string())?;
|
||||
let metadata = engine::meta::read_project_json(&workflow.data_dir)
|
||||
@@ -312,7 +493,7 @@ impl BdsApp {
|
||||
if workflow.kind == SiteGenerationKind::Validation {
|
||||
self.site_validation_state.is_applying = false;
|
||||
}
|
||||
self.add_output(&t(self.ui_locale, "engine.generationCancelled"));
|
||||
self.add_output(&t(self.ui_locale, "engine.generationCancellationRequested"));
|
||||
self.refresh_task_snapshots();
|
||||
true
|
||||
}
|
||||
@@ -337,7 +518,10 @@ impl BdsApp {
|
||||
let task_id = self.task_manager.submit(&label);
|
||||
self.search_index_rebuild_running = true;
|
||||
self.search_index_rebuild_task_id = Some(task_id);
|
||||
self.active_modal = Some(modal::ModalState::SearchIndexRebuilding);
|
||||
self.active_modal = Some(modal::ModalState::SearchIndexRebuilding {
|
||||
task_id,
|
||||
cancellation_requested: false,
|
||||
});
|
||||
self.refresh_task_snapshots();
|
||||
self.sync_menu_state();
|
||||
|
||||
@@ -346,10 +530,11 @@ impl BdsApp {
|
||||
let task_manager = Arc::clone(&self.task_manager);
|
||||
Task::perform(
|
||||
async move {
|
||||
let Some(worker) = task_manager.admit(task_id).await else {
|
||||
return Err("cancelled".to_string());
|
||||
};
|
||||
tokio::task::spawn_blocking(move || {
|
||||
if !task_manager.wait_until_runnable(task_id) {
|
||||
return Err("cancelled".to_string());
|
||||
}
|
||||
let _worker = worker;
|
||||
let db = Database::open(&db_path).map_err(|error| error.to_string())?;
|
||||
let progress_manager = Arc::clone(&task_manager);
|
||||
let on_item: engine::search::ItemProgressFn =
|
||||
@@ -373,6 +558,7 @@ impl BdsApp {
|
||||
Some(progress),
|
||||
Some(message),
|
||||
);
|
||||
!progress_manager.is_cancelled(task_id)
|
||||
});
|
||||
let report = engine::search::rebuild_search_index(db.conn(), Some(on_item))
|
||||
.map_err(|error| error.to_string())?;
|
||||
@@ -403,6 +589,49 @@ impl BdsApp {
|
||||
self.spawn_engine_task_in_group(label_key, None, work)
|
||||
}
|
||||
|
||||
pub(super) fn spawn_result_task<T, F, M>(
|
||||
&mut self,
|
||||
label_key: &'static str,
|
||||
work: F,
|
||||
message: M,
|
||||
) -> Task<Message>
|
||||
where
|
||||
T: Send + 'static,
|
||||
F: FnOnce(PathBuf, String, PathBuf, Arc<TaskManager>, TaskId) -> Result<T, String>
|
||||
+ Send
|
||||
+ 'static,
|
||||
M: Fn(TaskId, Result<T, String>) -> Message + Send + Sync + 'static,
|
||||
{
|
||||
let (Some(project_id), Some(data_dir)) = (
|
||||
self.active_project
|
||||
.as_ref()
|
||||
.map(|project| project.id.clone()),
|
||||
self.data_dir.clone(),
|
||||
) else {
|
||||
return Task::none();
|
||||
};
|
||||
let label = t(self.ui_locale, label_key);
|
||||
self.add_output(&label);
|
||||
let task_id = self.task_manager.submit(&label);
|
||||
self.refresh_task_snapshots();
|
||||
let task_manager = Arc::clone(&self.task_manager);
|
||||
let db_path = self.db_path.clone();
|
||||
Task::perform(
|
||||
async move {
|
||||
let Some(worker) = task_manager.admit(task_id).await else {
|
||||
return Err("cancelled".to_string());
|
||||
};
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let _worker = worker;
|
||||
work(db_path, project_id, data_dir, task_manager, task_id)
|
||||
})
|
||||
.await
|
||||
.unwrap_or_else(|error| Err(format!("task panicked: {error}")))
|
||||
},
|
||||
move |result| message(task_id, result),
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn spawn_grouped_engine_task<F>(
|
||||
&mut self,
|
||||
label_key: &'static str,
|
||||
@@ -453,10 +682,11 @@ impl BdsApp {
|
||||
|
||||
Task::perform(
|
||||
async move {
|
||||
let Some(worker) = tm.admit(task_id).await else {
|
||||
return Err("cancelled".to_string());
|
||||
};
|
||||
tokio::task::spawn_blocking(move || {
|
||||
if !tm.wait_until_runnable(task_id) {
|
||||
return Err("cancelled".to_string());
|
||||
}
|
||||
let _worker = worker;
|
||||
work(db_path, project_id, data_dir, tm, task_id)
|
||||
})
|
||||
.await
|
||||
@@ -490,6 +720,7 @@ fn run_site_generation_section(
|
||||
db_path: PathBuf,
|
||||
project_id: String,
|
||||
data_dir: PathBuf,
|
||||
prepared: Arc<engine::generation::PreparedSiteGeneration>,
|
||||
task_manager: Arc<TaskManager>,
|
||||
task_id: TaskId,
|
||||
section: engine::generation::GenerationSection,
|
||||
@@ -498,9 +729,6 @@ fn run_site_generation_section(
|
||||
locale: UiLocale,
|
||||
expected_pages: usize,
|
||||
) -> Result<engine::generation::GenerationReport, String> {
|
||||
if !task_manager.wait_until_runnable(task_id) {
|
||||
return Err("cancelled".to_string());
|
||||
}
|
||||
task_manager.report_progress(
|
||||
task_id,
|
||||
Some(if expected_pages == 0 { 1.0 } else { 0.0 }),
|
||||
@@ -511,26 +739,6 @@ fn run_site_generation_section(
|
||||
)),
|
||||
);
|
||||
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 posts = bds_core::db::queries::post::list_posts_by_project(db.conn(), &project_id)
|
||||
.map_err(|error| error.to_string())?;
|
||||
let sources = posts
|
||||
.into_iter()
|
||||
.filter(engine::generation::has_published_snapshot)
|
||||
.map(|post| engine::generation::load_published_post_source(&data_dir, post))
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|error| error.to_string())?
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect::<Vec<_>>();
|
||||
let prepared = engine::generation::prepare_site_generation(
|
||||
db.conn(),
|
||||
&data_dir,
|
||||
&project_id,
|
||||
&metadata,
|
||||
&sources,
|
||||
)
|
||||
.map_err(|error| error.to_string())?;
|
||||
let output_dir = data_dir.join("html");
|
||||
std::fs::create_dir_all(&output_dir).map_err(|error| error.to_string())?;
|
||||
let render_manager = Arc::clone(&task_manager);
|
||||
@@ -565,7 +773,7 @@ fn run_site_generation_section(
|
||||
db.conn(),
|
||||
&output_dir,
|
||||
&project_id,
|
||||
&prepared,
|
||||
prepared.as_ref(),
|
||||
&validation,
|
||||
section,
|
||||
on_page,
|
||||
@@ -576,7 +784,7 @@ fn run_site_generation_section(
|
||||
db.conn(),
|
||||
&output_dir,
|
||||
&project_id,
|
||||
&prepared,
|
||||
prepared.as_ref(),
|
||||
section,
|
||||
force,
|
||||
&on_rendered,
|
||||
|
||||
@@ -107,15 +107,23 @@ impl PanelTab {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TaskSnapshot {
|
||||
pub id: u64,
|
||||
pub source: TaskSource,
|
||||
pub label: String,
|
||||
pub group_id: Option<String>,
|
||||
pub group_name: Option<String>,
|
||||
pub status: String,
|
||||
pub status: bds_core::engine::task::TaskStatus,
|
||||
pub progress: Option<f32>,
|
||||
pub message: Option<String>,
|
||||
pub cancellation_requested: bool,
|
||||
pub is_cancellable: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum TaskSource {
|
||||
Local,
|
||||
Remote,
|
||||
}
|
||||
|
||||
/// A single line of output shown in the panel.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct OutputEntry {
|
||||
|
||||
@@ -386,7 +386,10 @@ fn network_output(run: &GitNetworkRunState, locale: UiLocale) -> Element<'static
|
||||
)
|
||||
.padding(6),
|
||||
button(text(t(locale, "common.cancel")).size(11))
|
||||
.on_press(Message::CancelTask(run.task_id))
|
||||
.on_press(Message::CancelTask(
|
||||
crate::state::navigation::TaskSource::Local,
|
||||
run.task_id,
|
||||
))
|
||||
.padding([4, 7])
|
||||
.style(inputs::secondary_button),
|
||||
]
|
||||
|
||||
@@ -101,19 +101,33 @@ pub fn view<'a>(state: &'a MetadataDiffState, locale: UiLocale) -> Element<'a, M
|
||||
));
|
||||
}
|
||||
if !report.orphans.is_empty() {
|
||||
let rows = report
|
||||
.orphans
|
||||
.iter()
|
||||
.fold(column!().spacing(4), |column, orphan| {
|
||||
column.push(
|
||||
let rows = report.orphans.iter().enumerate().fold(
|
||||
column!().spacing(8),
|
||||
|column, (index, orphan)| {
|
||||
let mut item = row![
|
||||
text(tw(
|
||||
locale,
|
||||
"metadataDiff.orphan",
|
||||
&[("reason", &orphan.reason), ("path", &orphan.file_path)],
|
||||
))
|
||||
.size(12),
|
||||
)
|
||||
});
|
||||
Space::with_width(Length::Fill),
|
||||
]
|
||||
.align_y(iced::Alignment::Center);
|
||||
if orphan.reason == "file_without_db_entry" {
|
||||
item = item.push(
|
||||
button(text(t(locale, "metadataDiff.importOrphan")).size(12))
|
||||
.on_press_maybe(
|
||||
(!state.is_repairing)
|
||||
.then_some(Message::ImportMetadataOrphan(index)),
|
||||
)
|
||||
.style(inputs::secondary_button)
|
||||
.padding([5, 10]),
|
||||
);
|
||||
}
|
||||
column.push(item)
|
||||
},
|
||||
);
|
||||
content = content.push(inputs::card(
|
||||
column![text(t(locale, "metadataDiff.orphans")).size(16), rows].spacing(8),
|
||||
));
|
||||
|
||||
@@ -64,7 +64,10 @@ pub enum ModalState {
|
||||
on_confirm: ConfirmAction,
|
||||
},
|
||||
SearchIndexRepair,
|
||||
SearchIndexRebuilding,
|
||||
SearchIndexRebuilding {
|
||||
task_id: bds_core::engine::task::TaskId,
|
||||
cancellation_requested: bool,
|
||||
},
|
||||
FindReplace {
|
||||
query: String,
|
||||
replacement: String,
|
||||
@@ -552,8 +555,11 @@ pub fn view(
|
||||
.into()
|
||||
}
|
||||
|
||||
ModalState::SearchIndexRebuilding => container(
|
||||
column![
|
||||
ModalState::SearchIndexRebuilding {
|
||||
task_id,
|
||||
cancellation_requested,
|
||||
} => {
|
||||
let mut content = column![
|
||||
text(t(locale, "searchIndexRepair.rebuildingTitle"))
|
||||
.size(16)
|
||||
.shaping(Shaping::Advanced)
|
||||
@@ -563,12 +569,31 @@ pub fn view(
|
||||
.size(13)
|
||||
.shaping(Shaping::Advanced)
|
||||
.color(Color::from_rgb(0.80, 0.80, 0.85)),
|
||||
]
|
||||
.padding(20),
|
||||
)
|
||||
.width(Length::Fixed(420.0))
|
||||
.style(modal_box_style)
|
||||
.into(),
|
||||
];
|
||||
content = if cancellation_requested {
|
||||
content.push(Space::with_height(16.0)).push(
|
||||
text(t(locale, "tasks.status.cancelling"))
|
||||
.size(13)
|
||||
.shaping(Shaping::Advanced)
|
||||
.color(Color::from_rgb(0.80, 0.80, 0.85)),
|
||||
)
|
||||
} else {
|
||||
content.push(Space::with_height(16.0)).push(row![
|
||||
Space::with_width(Length::Fill),
|
||||
button(text(t(locale, "tasks.cancelTask")).size(13))
|
||||
.on_press(Message::CancelTask(
|
||||
crate::state::navigation::TaskSource::Local,
|
||||
task_id,
|
||||
))
|
||||
.padding([6, 16])
|
||||
.style(cancel_button_style),
|
||||
])
|
||||
};
|
||||
container(content.padding(20))
|
||||
.width(Length::Fixed(420.0))
|
||||
.style(modal_box_style)
|
||||
.into()
|
||||
}
|
||||
|
||||
ModalState::PostInsertLink {
|
||||
post_id: _post_id,
|
||||
|
||||
@@ -3,17 +3,28 @@ use iced::widget::{Space, button, column, container, row, scrollable, text};
|
||||
use iced::{Alignment, Background, Border, Color, Element, Length, Theme};
|
||||
|
||||
use bds_core::engine::git::GitCommit;
|
||||
use bds_core::engine::task::TaskStatus;
|
||||
use bds_core::i18n::UiLocale;
|
||||
|
||||
use crate::app::Message;
|
||||
use crate::components::inputs;
|
||||
use crate::i18n::t;
|
||||
use crate::i18n::{t, tw};
|
||||
use crate::state::navigation::{OutputEntry, PanelTab, TaskSnapshot};
|
||||
use crate::state::tabs::{Tab, TabType};
|
||||
use crate::views::post_editor::ResolvedPostLink;
|
||||
use std::collections::HashSet;
|
||||
|
||||
fn task_row(snapshot: &TaskSnapshot, locale: UiLocale) -> Element<'static, Message> {
|
||||
let status = match &snapshot.status {
|
||||
TaskStatus::Pending => t(locale, "tasks.statusPending"),
|
||||
TaskStatus::Running if snapshot.cancellation_requested => {
|
||||
t(locale, "tasks.statusCancelling")
|
||||
}
|
||||
TaskStatus::Running => t(locale, "tasks.statusRunning"),
|
||||
TaskStatus::Completed => t(locale, "tasks.statusCompleted"),
|
||||
TaskStatus::Failed(error) => tw(locale, "tasks.statusFailed", &[("error", error)]),
|
||||
TaskStatus::Cancelled => t(locale, "tasks.statusCancelled"),
|
||||
};
|
||||
let progress = snapshot
|
||||
.progress
|
||||
.map(|value| format!(" ({:.0}%)", value * 100.0))
|
||||
@@ -25,7 +36,7 @@ fn task_row(snapshot: &TaskSnapshot, locale: UiLocale) -> Element<'static, Messa
|
||||
.unwrap_or_default();
|
||||
let label = text(format!(
|
||||
"{} — {}{}{}",
|
||||
snapshot.label, snapshot.status, progress, phase
|
||||
snapshot.label, status, progress, phase
|
||||
))
|
||||
.size(11)
|
||||
.shaping(Shaping::Advanced)
|
||||
@@ -34,7 +45,7 @@ fn task_row(snapshot: &TaskSnapshot, locale: UiLocale) -> Element<'static, Messa
|
||||
if snapshot.is_cancellable {
|
||||
content = content.push(Space::with_width(Length::Fill)).push(
|
||||
button(text(t(locale, "tasks.cancelTask")).size(11))
|
||||
.on_press(Message::CancelTask(snapshot.id))
|
||||
.on_press(Message::CancelTask(snapshot.source, snapshot.id))
|
||||
.padding([3, 8])
|
||||
.style(inputs::secondary_button),
|
||||
);
|
||||
@@ -46,12 +57,9 @@ fn group_progress(members: &[&TaskSnapshot]) -> Option<f32> {
|
||||
(!members.is_empty()).then(|| {
|
||||
members
|
||||
.iter()
|
||||
.map(|member| {
|
||||
if member.is_cancellable {
|
||||
member.progress.unwrap_or(0.0)
|
||||
} else {
|
||||
1.0
|
||||
}
|
||||
.map(|member| match member.status {
|
||||
TaskStatus::Completed | TaskStatus::Failed(_) | TaskStatus::Cancelled => 1.0,
|
||||
TaskStatus::Pending | TaskStatus::Running => member.progress.unwrap_or(0.0),
|
||||
})
|
||||
.sum::<f32>()
|
||||
/ members.len() as f32
|
||||
@@ -219,7 +227,7 @@ pub fn view(
|
||||
.padding(8)
|
||||
.into()
|
||||
} else {
|
||||
let visible = task_snapshots.iter().rev().take(10).collect::<Vec<_>>();
|
||||
let visible = task_snapshots.iter().collect::<Vec<_>>();
|
||||
let mut rendered_groups = HashSet::new();
|
||||
let mut items: Vec<Element<'static, Message>> = Vec::new();
|
||||
for snapshot in &visible {
|
||||
@@ -441,16 +449,19 @@ mod progress_tests {
|
||||
fn group_progress_includes_pending_tasks_as_zero_like_bds2() {
|
||||
let complete = TaskSnapshot {
|
||||
id: 1,
|
||||
source: crate::state::navigation::TaskSource::Local,
|
||||
label: String::new(),
|
||||
group_id: None,
|
||||
group_name: None,
|
||||
status: String::new(),
|
||||
status: TaskStatus::Completed,
|
||||
progress: Some(1.0),
|
||||
message: None,
|
||||
cancellation_requested: false,
|
||||
is_cancellable: false,
|
||||
};
|
||||
let pending = TaskSnapshot {
|
||||
id: 2,
|
||||
status: TaskStatus::Pending,
|
||||
progress: None,
|
||||
is_cancellable: true,
|
||||
..complete.clone()
|
||||
|
||||
@@ -2,10 +2,11 @@ use iced::widget::text::Shaping;
|
||||
use iced::widget::{Space, button, container, row, text};
|
||||
use iced::{Alignment, Background, Border, Color, Element, Length, Theme};
|
||||
|
||||
use bds_core::engine::task::TaskStatus;
|
||||
use bds_core::i18n::UiLocale;
|
||||
|
||||
use crate::app::Message;
|
||||
use crate::i18n::tw;
|
||||
use crate::i18n::{t, tw};
|
||||
use crate::state::navigation::TaskSnapshot;
|
||||
use crate::views::project_selector;
|
||||
|
||||
@@ -131,23 +132,38 @@ pub fn view(
|
||||
let project_name = active_project_name.unwrap_or("\u{2014}");
|
||||
let project_trigger = project_selector::trigger_button(project_name);
|
||||
|
||||
let running: Vec<&TaskSnapshot> = task_snapshots
|
||||
let active: Vec<&TaskSnapshot> = task_snapshots
|
||||
.iter()
|
||||
.filter(|t| t.status == "running")
|
||||
.filter(|task| matches!(task.status, TaskStatus::Pending | TaskStatus::Running))
|
||||
.collect();
|
||||
let task_indicator: Element<'static, Message> = if !running.is_empty() {
|
||||
let first = &running[0];
|
||||
let first = active
|
||||
.iter()
|
||||
.find(|task| task.status == TaskStatus::Running)
|
||||
.or_else(|| active.first());
|
||||
let task_indicator: Element<'static, Message> = if let Some(first) = first {
|
||||
let progress_str = first
|
||||
.progress
|
||||
.map(|p| format!(" {:.0}%", p * 100.0))
|
||||
.unwrap_or_default();
|
||||
let phase_str = first.message.as_deref().unwrap_or("");
|
||||
let extra = if running.len() > 1 {
|
||||
format!(" (+{})", running.len() - 1)
|
||||
let extra = if active.len() > 1 {
|
||||
format!(" (+{})", active.len() - 1)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
let display = if phase_str.is_empty() {
|
||||
let display = if first.cancellation_requested {
|
||||
format!(
|
||||
"{}: {}{progress_str}{extra}",
|
||||
t(locale, "tasks.statusCancelling"),
|
||||
first.label
|
||||
)
|
||||
} else if first.status == TaskStatus::Pending {
|
||||
format!(
|
||||
"{}: {}{extra}",
|
||||
t(locale, "tasks.statusPending"),
|
||||
first.label
|
||||
)
|
||||
} else if phase_str.is_empty() {
|
||||
format!("{}{progress_str}{extra}", first.label)
|
||||
} else {
|
||||
format!("{phase_str}{progress_str}{extra}")
|
||||
|
||||
Reference in New Issue
Block a user