From 989efeaf255b51c53f1286a807e017bcf0d13f2f Mon Sep 17 00:00:00 2001 From: Chili Palmer Date: Sat, 4 Apr 2026 21:46:56 +0200 Subject: [PATCH] fix: loading of data from the database seems to do something --- crates/bds-core/src/db/queries/media.rs | 8 +++ crates/bds-core/src/engine/media.rs | 35 +++++++++--- crates/bds-core/src/engine/post.rs | 30 ++++++++-- crates/bds-core/src/engine/rebuild.rs | 63 ++++++++++++++++++-- crates/bds-core/src/util/checksum.rs | 17 ++++++ crates/bds-core/src/util/mod.rs | 2 +- crates/bds-core/src/util/thumbnail.rs | 13 +++-- crates/bds-ui/src/app.rs | 72 +++++++++++++++++++---- crates/bds-ui/src/views/panel.rs | 14 +++-- crates/bds-ui/src/views/sidebar.rs | 76 ++++++++++++++++++++++--- crates/bds-ui/src/views/status_bar.rs | 22 +++++-- crates/bds-ui/src/views/workspace.rs | 6 +- 12 files changed, 303 insertions(+), 55 deletions(-) diff --git a/crates/bds-core/src/db/queries/media.rs b/crates/bds-core/src/db/queries/media.rs index 6f060fa..c2b6b73 100644 --- a/crates/bds-core/src/db/queries/media.rs +++ b/crates/bds-core/src/db/queries/media.rs @@ -59,6 +59,14 @@ pub fn list_media_by_project(conn: &Connection, project_id: &str) -> rusqlite::R rows.collect() } +pub fn count_media_by_project(conn: &Connection, project_id: &str) -> rusqlite::Result { + conn.query_row( + "SELECT COUNT(*) FROM media WHERE project_id = ?1", + params![project_id], + |row| row.get(0), + ) +} + pub fn update_media(conn: &Connection, m: &Media) -> rusqlite::Result<()> { conn.execute( "UPDATE media SET diff --git a/crates/bds-core/src/engine/media.rs b/crates/bds-core/src/engine/media.rs index 32d6dfc..e815615 100644 --- a/crates/bds-core/src/engine/media.rs +++ b/crates/bds-core/src/engine/media.rs @@ -328,6 +328,19 @@ pub fn rebuild_media_from_filesystem( conn: &Connection, data_dir: &Path, project_id: &str, +) -> EngineResult { + rebuild_media_from_filesystem_with_progress(conn, data_dir, project_id, None) +} + +/// Per-item progress callback: (current_item, total_items, item_description). +pub type ItemProgressFn = Box; + +/// Like `rebuild_media_from_filesystem` but with optional per-item progress. +pub fn rebuild_media_from_filesystem_with_progress( + conn: &Connection, + data_dir: &Path, + project_id: &str, + on_item: Option, ) -> EngineResult { let mut report = MediaRebuildReport::default(); let media_dir = data_dir.join("media"); @@ -356,8 +369,6 @@ pub fn rebuild_media_from_filesystem( continue; } - // Determine if this is a translation sidecar: {name}.{lang}.meta - // where lang is exactly 2 lowercase letters. if is_translation_sidecar(file_name) { translation_sidecars.push(path.to_path_buf()); } else { @@ -365,8 +376,14 @@ pub fn rebuild_media_from_filesystem( } } + let total = canonical_sidecars.len() + translation_sidecars.len(); + // Process canonical sidecars first - for path in &canonical_sidecars { + for (i, path) in canonical_sidecars.iter().enumerate() { + if let Some(ref cb) = on_item { + let name = path.file_stem().and_then(|s| s.to_str()).unwrap_or("?"); + cb(i + 1, total, name); + } match rebuild_canonical_media(conn, data_dir, project_id, path) { Ok(created) => { if created { @@ -382,7 +399,12 @@ pub fn rebuild_media_from_filesystem( } // Process translation sidecars - for path in &translation_sidecars { + let offset = canonical_sidecars.len(); + for (i, path) in translation_sidecars.iter().enumerate() { + if let Some(ref cb) = on_item { + let name = path.file_stem().and_then(|s| s.to_str()).unwrap_or("?"); + cb(offset + i + 1, total, name); + } match rebuild_translation_sidecar(conn, data_dir, project_id, path) { Ok(created) => { if created { @@ -494,10 +516,9 @@ fn rebuild_canonical_media( sc.size }; - // Get checksum (if file exists) + // Get checksum (if file exists) — streamed to avoid loading large files into memory let checksum = if abs_file.exists() { - let bytes = fs::read(&abs_file)?; - Some(content_hash(&bytes)) + Some(crate::util::file_hash(&abs_file)?) } else { None }; diff --git a/crates/bds-core/src/engine/post.rs b/crates/bds-core/src/engine/post.rs index eb30cef..60810a5 100644 --- a/crates/bds-core/src/engine/post.rs +++ b/crates/bds-core/src/engine/post.rs @@ -451,6 +451,19 @@ pub fn rebuild_posts_from_filesystem( conn: &Connection, data_dir: &Path, project_id: &str, +) -> EngineResult { + rebuild_posts_from_filesystem_with_progress(conn, data_dir, project_id, None) +} + +/// Per-item progress callback: (current_item, total_items, item_description). +pub type ItemProgressFn = Box; + +/// Like `rebuild_posts_from_filesystem` but with optional per-item progress. +pub fn rebuild_posts_from_filesystem_with_progress( + conn: &Connection, + data_dir: &Path, + project_id: &str, + on_item: Option, ) -> EngineResult { let mut report = RebuildReport::default(); let posts_dir = data_dir.join("posts"); @@ -477,8 +490,6 @@ pub fn rebuild_posts_from_filesystem( } let stem = path.file_stem().and_then(|s| s.to_str()).unwrap_or(""); - // Check if it's a translation: {slug}.{lang}.md - // The stem would be {slug}.{lang} if is_translation_filename(stem) { translation_files.push(path.to_path_buf()); } else { @@ -486,8 +497,14 @@ pub fn rebuild_posts_from_filesystem( } } + let total = canonical_files.len() + translation_files.len(); + // Process canonical posts first - for path in &canonical_files { + for (i, path) in canonical_files.iter().enumerate() { + if let Some(ref cb) = on_item { + let name = path.file_stem().and_then(|s| s.to_str()).unwrap_or("?"); + cb(i + 1, total, name); + } match rebuild_canonical_post(conn, data_dir, project_id, path) { Ok(created) => { if created { @@ -503,7 +520,12 @@ pub fn rebuild_posts_from_filesystem( } // Process translations - for path in &translation_files { + let offset = canonical_files.len(); + for (i, path) in translation_files.iter().enumerate() { + if let Some(ref cb) = on_item { + let name = path.file_stem().and_then(|s| s.to_str()).unwrap_or("?"); + cb(offset + i + 1, total, name); + } match rebuild_translation(conn, data_dir, project_id, path) { Ok(created) => { if created { diff --git a/crates/bds-core/src/engine/rebuild.rs b/crates/bds-core/src/engine/rebuild.rs index 57d6af1..13e89af 100644 --- a/crates/bds-core/src/engine/rebuild.rs +++ b/crates/bds-core/src/engine/rebuild.rs @@ -1,4 +1,5 @@ use std::path::Path; +use std::sync::Arc; use rusqlite::Connection; @@ -27,6 +28,9 @@ pub struct FullRebuildReport { pub errors: Vec, } +/// Progress callback: (percent 0.0..1.0, phase description). +pub type ProgressFn = Arc; + /// Orchestrate a full rebuild from filesystem into the database. /// /// Ensures FTS tables exist, then rebuilds posts, media, templates, and scripts @@ -35,42 +39,89 @@ pub fn rebuild_from_filesystem( conn: &Connection, data_dir: &Path, project_id: &str, +) -> EngineResult { + rebuild_from_filesystem_with_progress(conn, data_dir, project_id, None) +} + +/// Like `rebuild_from_filesystem` but accepts an optional progress callback. +pub fn rebuild_from_filesystem_with_progress( + conn: &Connection, + data_dir: &Path, + project_id: &str, + on_progress: Option, ) -> EngineResult { let mut report = FullRebuildReport::default(); + let progress = |pct: f32, msg: &str| { + if let Some(ref f) = on_progress { + f(pct, msg); + } + }; + + // Phase weights: posts 0.0..0.35, media 0.35..0.70, templates 0.70..0.85, scripts 0.85..1.0 // 1. Ensure FTS tables exist + progress(0.0, "Ensuring FTS tables..."); fts::ensure_fts_tables(conn)?; - // 2. Rebuild posts - let post_report = post::rebuild_posts_from_filesystem(conn, data_dir, project_id)?; + // 2. Rebuild posts (0.00 .. 0.35) + progress(0.01, "Scanning posts..."); + let post_item_cb: Option = on_progress.as_ref().map(|cb| { + let cb = Arc::clone(cb); + let f: post::ItemProgressFn = Box::new(move |current, total, name| { + let phase_pct = if total > 0 { current as f32 / total as f32 } else { 1.0 }; + let global_pct = 0.01 + phase_pct * 0.34; + let msg = format!("Posts: {current}/{total} \u{2014} {name}"); + cb(global_pct, &msg); + }); + f + }); + let post_report = post::rebuild_posts_from_filesystem_with_progress( + conn, data_dir, project_id, post_item_cb, + )?; report.posts_created = post_report.posts_created; report.posts_updated = post_report.posts_updated; report.translations_created = post_report.translations_created; report.translations_updated = post_report.translations_updated; report.errors.extend(post_report.errors); - // 3. Rebuild media - let media_report = media::rebuild_media_from_filesystem(conn, data_dir, project_id)?; + // 3. Rebuild media (0.35 .. 0.70) + progress(0.35, "Scanning media..."); + let media_item_cb: Option = on_progress.as_ref().map(|cb| { + let cb = Arc::clone(cb); + let f: media::ItemProgressFn = Box::new(move |current, total, name| { + let phase_pct = if total > 0 { current as f32 / total as f32 } else { 1.0 }; + let global_pct = 0.35 + phase_pct * 0.35; + let msg = format!("Media: {current}/{total} \u{2014} {name}"); + cb(global_pct, &msg); + }); + f + }); + let media_report = media::rebuild_media_from_filesystem_with_progress( + conn, data_dir, project_id, media_item_cb, + )?; report.media_created = media_report.media_created; report.media_updated = media_report.media_updated; report.media_translations_created = media_report.translations_created; report.media_translations_updated = media_report.translations_updated; report.errors.extend(media_report.errors); - // 4. Rebuild templates + // 4. Rebuild templates (0.70 .. 0.85) + progress(0.70, "Rebuilding templates..."); let tpl_report = template_rebuild::rebuild_templates_from_filesystem(conn, data_dir, project_id)?; report.templates_created = tpl_report.created; report.templates_updated = tpl_report.updated; report.errors.extend(tpl_report.errors); - // 5. Rebuild scripts + // 5. Rebuild scripts (0.85 .. 1.0) + progress(0.85, "Rebuilding scripts..."); let script_report = script_rebuild::rebuild_scripts_from_filesystem(conn, data_dir, project_id)?; report.scripts_created = script_report.created; report.scripts_updated = script_report.updated; report.errors.extend(script_report.errors); + progress(1.0, "Rebuild complete"); Ok(report) } diff --git a/crates/bds-core/src/util/checksum.rs b/crates/bds-core/src/util/checksum.rs index 22bead8..26b1561 100644 --- a/crates/bds-core/src/util/checksum.rs +++ b/crates/bds-core/src/util/checksum.rs @@ -1,4 +1,6 @@ use sha2::{Digest, Sha256}; +use std::io::Read; +use std::path::Path; /// Compute a hex-encoded SHA-256 hash of the given content. pub fn content_hash(content: &[u8]) -> String { @@ -8,6 +10,21 @@ pub fn content_hash(content: &[u8]) -> String { hex::encode(result) } +/// Compute a hex-encoded SHA-256 hash of a file by streaming (8 KB chunks). +pub fn file_hash(path: &Path) -> std::io::Result { + let mut file = std::fs::File::open(path)?; + let mut hasher = Sha256::new(); + let mut buf = [0u8; 8192]; + loop { + let n = file.read(&mut buf)?; + if n == 0 { + break; + } + hasher.update(&buf[..n]); + } + Ok(hex::encode(hasher.finalize())) +} + // sha2 doesn't include hex encoding, so we use a tiny inline helper. mod hex { const HEX_CHARS: &[u8; 16] = b"0123456789abcdef"; diff --git a/crates/bds-core/src/util/mod.rs b/crates/bds-core/src/util/mod.rs index a9323d2..25be624 100644 --- a/crates/bds-core/src/util/mod.rs +++ b/crates/bds-core/src/util/mod.rs @@ -8,7 +8,7 @@ pub mod sidecar; pub mod thumbnail; pub use slug::{slugify, ensure_unique}; -pub use checksum::content_hash; +pub use checksum::{content_hash, file_hash}; pub use timestamp::{unix_ms_to_iso, iso_to_unix_ms, year_month_from_unix_ms, year_month_day_from_unix_ms, now_unix_ms}; pub use atomic_write::{atomic_write, atomic_write_str}; pub use paths::*; diff --git a/crates/bds-core/src/util/thumbnail.rs b/crates/bds-core/src/util/thumbnail.rs index e0c1ff7..8d39d58 100644 --- a/crates/bds-core/src/util/thumbnail.rs +++ b/crates/bds-core/src/util/thumbnail.rs @@ -236,15 +236,16 @@ fn apply_orientation(img: DynamicImage, orientation: u16) -> DynamicImage { } } -/// Extract image dimensions from a file. +/// Extract image dimensions from a file (header-only, no full decode). pub fn image_dimensions(path: &Path) -> Result<(u32, u32), String> { - let img = ImageReader::open(path) + let reader = ImageReader::open(path) .map_err(|e| format!("open: {e}"))? .with_guessed_format() - .map_err(|e| format!("format: {e}"))? - .decode() - .map_err(|e| format!("decode: {e}"))?; - Ok(img.dimensions()) + .map_err(|e| format!("format: {e}"))?; + let (w, h) = reader + .into_dimensions() + .map_err(|e| format!("dimensions: {e}"))?; + Ok((w, h)) } /// Detect MIME type from file extension. diff --git a/crates/bds-ui/src/app.rs b/crates/bds-ui/src/app.rs index ae284b9..b1c9a0f 100644 --- a/crates/bds-ui/src/app.rs +++ b/crates/bds-ui/src/app.rs @@ -7,7 +7,7 @@ use bds_core::db::Database; use bds_core::engine::task::{TaskId, TaskManager, TaskStatus}; use bds_core::engine; use bds_core::i18n::{detect_os_locale, UiLocale}; -use bds_core::model::Project; +use bds_core::model::{Post, Project}; use crate::i18n::{t, tw}; use crate::platform::menu::{self, MenuAction, MenuRegistry}; @@ -94,6 +94,13 @@ pub struct BdsApp { projects: Vec, data_dir: Option, + // Counts + post_count: usize, + media_count: usize, + + // Sidebar data + sidebar_posts: Vec, + // Navigation sidebar_view: SidebarView, sidebar_visible: bool, @@ -215,6 +222,9 @@ impl BdsApp { active_project: active_project.clone(), projects, data_dir, + post_count: 0, + media_count: 0, + sidebar_posts: Vec::new(), sidebar_view: SidebarView::Posts, sidebar_visible: true, tabs: Vec::new(), @@ -305,6 +315,7 @@ impl BdsApp { .and_then(|p| p.data_path.as_ref()) .map(PathBuf::from); } + self.refresh_counts(); Task::none() } Message::SwitchProject(project_id) => { @@ -459,10 +470,14 @@ impl BdsApp { Message::RebuildDatabase => { self.spawn_engine_task( "engine.rebuildStarted", - |db_path, project_id, data_dir| { + |db_path, project_id, data_dir, tm, tid| { let db = Database::open(&db_path).map_err(|e| e.to_string())?; - let report = engine::rebuild::rebuild_from_filesystem(db.conn(), &data_dir, &project_id) - .map_err(|e| e.to_string())?; + let on_progress: engine::rebuild::ProgressFn = Arc::new(move |pct, msg| { + tm.report_progress(tid, Some(pct), Some(msg.to_string())); + }); + let report = engine::rebuild::rebuild_from_filesystem_with_progress( + db.conn(), &data_dir, &project_id, Some(on_progress), + ).map_err(|e| e.to_string())?; let posts = report.posts_created + report.posts_updated; let media = report.media_created + report.media_updated; let templates = report.templates_created + report.templates_updated; @@ -474,12 +489,14 @@ impl BdsApp { Message::ReindexText => { self.spawn_engine_task( "engine.reindexStarted", - |db_path, project_id, data_dir| { + |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.0), Some("Reading project config...".into())); let main_lang = engine::meta::read_project_json(&data_dir) .ok() .and_then(|m| m.main_language) .unwrap_or_else(|| "en".to_string()); + tm.report_progress(tid, Some(0.10), Some("Reindexing...".into())); let report = engine::search::reindex_all(db.conn(), &project_id, &main_lang) .map_err(|e| e.to_string())?; Ok(format!("posts={}, media={}", report.posts_indexed, report.media_indexed)) @@ -489,8 +506,9 @@ impl BdsApp { Message::RegenerateCalendar => { self.spawn_engine_task( "engine.calendarStarted", - |db_path, project_id, data_dir| { + |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.10), Some("Generating calendar JSON...".into())); engine::calendar::regenerate_calendar(db.conn(), &data_dir, &project_id) .map_err(|e| e.to_string())?; Ok("done".to_string()) @@ -501,8 +519,9 @@ impl BdsApp { self.open_singleton_tab(TabType::TranslationValidation, "Translation Validation"); self.spawn_engine_task( "engine.validateTranslationsStarted", - |db_path, project_id, data_dir| { + |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.10), Some("Checking translations...".into())); let report = engine::validate_translations::validate_translations( db.conn(), &data_dir, &project_id, ).map_err(|e| e.to_string())?; @@ -513,8 +532,9 @@ impl BdsApp { Message::GenerateSite => { self.spawn_engine_task( "engine.generateSiteStarted", - |db_path, project_id, data_dir| { + |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.10), Some("Generating calendar...".into())); engine::calendar::regenerate_calendar(db.conn(), &data_dir, &project_id) .map_err(|e| e.to_string())?; Ok("done".to_string()) @@ -536,6 +556,7 @@ impl BdsApp { self.add_output(&format!("{label} failed: {err}")); } } + self.refresh_counts(); self.refresh_task_snapshots(); Task::none() } @@ -561,11 +582,12 @@ impl BdsApp { self.panel_tab, &self.task_snapshots, &self.output_entries, + &self.sidebar_posts, active_name, &self.projects, self.active_project.as_ref().map(|p| p.id.as_str()), - 0, // post_count — populated in later milestones - 0, // media_count — populated in later milestones + self.post_count, + self.media_count, self.offline_mode, self.locale_dropdown_open, self.project_dropdown_open, @@ -769,13 +791,18 @@ impl BdsApp { /// Returns `Task::none()` if no active project/db/data_dir. /// Otherwise registers the task, logs the start message, and returns an /// async `Task` that opens a fresh DB connection on a worker thread. + /// + /// The closure receives `(db_path, project_id, data_dir, task_manager, task_id)`. + /// Use `task_manager.report_progress(task_id, percent, message)` for live updates. fn spawn_engine_task( &mut self, label_key: &str, work: F, ) -> Task where - F: FnOnce(PathBuf, String, PathBuf) -> Result + Send + 'static, + F: FnOnce(PathBuf, String, PathBuf, Arc, TaskId) -> Result + + Send + + 'static, { let (Some(project), Some(data_dir)) = (&self.active_project, &self.data_dir) else { return Task::none(); @@ -792,10 +819,11 @@ impl BdsApp { self.refresh_task_snapshots(); let label_for_msg = label.clone(); + let tm = Arc::clone(&self.task_manager); Task::perform( async move { - tokio::task::spawn_blocking(move || work(db_path, project_id, data_dir)) + tokio::task::spawn_blocking(move || work(db_path, project_id, data_dir, tm, task_id)) .await .unwrap_or_else(|e| Err(format!("task panicked: {e}"))) }, @@ -806,4 +834,24 @@ impl BdsApp { }, ) } + + fn refresh_counts(&mut self) { + if let (Some(db), Some(project)) = (&self.db, &self.active_project) { + self.post_count = bds_core::db::queries::post::count_posts_by_project( + db.conn(), + &project.id, + ) + .unwrap_or(0) as usize; + self.media_count = bds_core::db::queries::media::count_media_by_project( + db.conn(), + &project.id, + ) + .unwrap_or(0) as usize; + self.sidebar_posts = bds_core::db::queries::post::list_posts_by_project( + db.conn(), + &project.id, + ) + .unwrap_or_default(); + } + } } diff --git a/crates/bds-ui/src/views/panel.rs b/crates/bds-ui/src/views/panel.rs index da63848..3127023 100644 --- a/crates/bds-ui/src/views/panel.rs +++ b/crates/bds-ui/src/views/panel.rs @@ -106,13 +106,19 @@ pub fn view( let items: Vec> = task_snapshots .iter() .map(|snap| { + let progress_str = snap.progress + .map(|p| format!(" ({:.0}%)", p * 100.0)) + .unwrap_or_default(); + let phase_str = snap.message + .as_deref() + .map(|m| format!(" \u{2014} {m}")) + .unwrap_or_default(); let status_text = format!( - "{} \u{2014} {}{}", + "{} \u{2014} {}{}{}", snap.label, snap.status, - snap.progress - .map(|p| format!(" ({:.0}%)", p * 100.0)) - .unwrap_or_default(), + progress_str, + phase_str, ); text(status_text).size(11).color(Color::from_rgb(0.70, 0.70, 0.75)).into() }) diff --git a/crates/bds-ui/src/views/sidebar.rs b/crates/bds-ui/src/views/sidebar.rs index 6fac3b7..5e64257 100644 --- a/crates/bds-ui/src/views/sidebar.rs +++ b/crates/bds-ui/src/views/sidebar.rs @@ -1,11 +1,13 @@ -use iced::widget::{column, container, scrollable, text, Space}; +use iced::widget::{button, column, container, scrollable, text, Space}; use iced::{Background, Border, Color, Element, Length, Theme}; use bds_core::i18n::UiLocale; +use bds_core::model::Post; use crate::app::Message; use crate::i18n::t; use crate::state::navigation::SidebarView; +use crate::state::tabs::{Tab, TabType}; /// Sidebar container style — dark background with right border separator. fn sidebar_style(_theme: &Theme) -> container::Style { @@ -20,6 +22,20 @@ fn sidebar_style(_theme: &Theme) -> container::Style { } } +/// Sidebar item button style. +fn item_style(_theme: &Theme, status: button::Status) -> button::Style { + let bg = match status { + button::Status::Hovered => Color::from_rgb(0.22, 0.22, 0.27), + _ => Color::TRANSPARENT, + }; + button::Style { + background: Some(Background::Color(bg)), + text_color: Color::from_rgb(0.80, 0.80, 0.85), + border: Border::default(), + ..button::Style::default() + } +} + /// Get the appropriate empty-state message key for each sidebar view. fn placeholder_key(view: SidebarView) -> &'static str { match view { @@ -38,19 +54,63 @@ fn placeholder_key(view: SidebarView) -> &'static str { pub fn view( sidebar_view: SidebarView, + posts: &[Post], locale: UiLocale, ) -> Element<'static, Message> { let header_text = t(locale, sidebar_view.i18n_key()); - let placeholder = t(locale, placeholder_key(sidebar_view)); + let muted = Color::from_rgb(0.50, 0.50, 0.55); + + let header = text(header_text) + .size(13) + .color(Color::from_rgb(0.85, 0.85, 0.90)); + + let body: Element<'static, Message> = match sidebar_view { + SidebarView::Posts => { + if posts.is_empty() { + text(t(locale, placeholder_key(sidebar_view))) + .size(12) + .color(muted) + .into() + } else { + let items: Vec> = posts + .iter() + .map(|p| { + let status_indicator = match p.status { + bds_core::model::PostStatus::Draft => "\u{25CB} ", // ○ + bds_core::model::PostStatus::Published => "\u{25CF} ", // ● + bds_core::model::PostStatus::Archived => "\u{25A1} ", // □ + }; + let label = format!("{status_indicator}{}", p.title); + button(text(label).size(12)) + .on_press(Message::OpenTab(Tab { + id: p.id.clone(), + tab_type: TabType::Post, + title: p.title.clone(), + is_transient: true, + })) + .padding([3, 6]) + .width(Length::Fill) + .style(item_style) + .into() + }) + .collect(); + iced::widget::Column::with_children(items) + .spacing(1) + .into() + } + } + _ => { + text(t(locale, placeholder_key(sidebar_view))) + .size(12) + .color(muted) + .into() + } + }; let content = column![ - text(header_text) - .size(13) - .color(Color::from_rgb(0.85, 0.85, 0.90)), + header, Space::with_height(8.0), - text(placeholder) - .size(12) - .color(Color::from_rgb(0.50, 0.50, 0.55)), + body, ] .spacing(4) .padding(12); diff --git a/crates/bds-ui/src/views/status_bar.rs b/crates/bds-ui/src/views/status_bar.rs index 35b3fdd..cf46fc2 100644 --- a/crates/bds-ui/src/views/status_bar.rs +++ b/crates/bds-ui/src/views/status_bar.rs @@ -129,12 +129,24 @@ pub fn view( .filter(|t| t.status == "running") .collect(); let task_indicator: Element<'static, Message> = if !running.is_empty() { - let first = running[0].label.clone(); - if running.len() > 1 { - text(format!("{first} (+{})", running.len() - 1)).size(11).color(label_color).into() + let first = &running[0]; + 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) } else { - text(first).size(11).color(label_color).into() - } + String::new() + }; + let display = if phase_str.is_empty() { + format!("{}{progress_str}{extra}", first.label) + } else { + format!("{phase_str}{progress_str}{extra}") + }; + text(display).size(11).color(label_color).into() } else { Space::with_width(0).into() }; diff --git a/crates/bds-ui/src/views/workspace.rs b/crates/bds-ui/src/views/workspace.rs index 5568061..c99af87 100644 --- a/crates/bds-ui/src/views/workspace.rs +++ b/crates/bds-ui/src/views/workspace.rs @@ -3,7 +3,7 @@ use iced::widget::text::Shaping; use iced::{Alignment, Background, Color, Element, Length, Padding, Theme}; use bds_core::i18n::UiLocale; -use bds_core::model::Project; +use bds_core::model::{Post, Project}; use crate::app::Message; use crate::state::navigation::{OutputEntry, PanelTab, SidebarView, TaskSnapshot}; @@ -53,6 +53,8 @@ pub fn view( panel_tab: PanelTab, task_snapshots: &[TaskSnapshot], output_entries: &[OutputEntry], + // Sidebar data + sidebar_posts: &[Post], // Status bar active_project_name: Option<&str>, projects: &[Project], @@ -89,7 +91,7 @@ pub fn view( let mut main_row = row![activity]; if sidebar_visible { - main_row = main_row.push(sidebar::view(sidebar_view, locale)); + main_row = main_row.push(sidebar::view(sidebar_view, sidebar_posts, locale)); main_row = main_row.push(separator_v()); }