fix: loading of data from the database seems to do something
This commit is contained in:
@@ -59,6 +59,14 @@ pub fn list_media_by_project(conn: &Connection, project_id: &str) -> rusqlite::R
|
|||||||
rows.collect()
|
rows.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn count_media_by_project(conn: &Connection, project_id: &str) -> rusqlite::Result<i64> {
|
||||||
|
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<()> {
|
pub fn update_media(conn: &Connection, m: &Media) -> rusqlite::Result<()> {
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"UPDATE media SET
|
"UPDATE media SET
|
||||||
|
|||||||
@@ -328,6 +328,19 @@ pub fn rebuild_media_from_filesystem(
|
|||||||
conn: &Connection,
|
conn: &Connection,
|
||||||
data_dir: &Path,
|
data_dir: &Path,
|
||||||
project_id: &str,
|
project_id: &str,
|
||||||
|
) -> EngineResult<MediaRebuildReport> {
|
||||||
|
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<dyn Fn(usize, usize, &str) + Send>;
|
||||||
|
|
||||||
|
/// 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<ItemProgressFn>,
|
||||||
) -> EngineResult<MediaRebuildReport> {
|
) -> EngineResult<MediaRebuildReport> {
|
||||||
let mut report = MediaRebuildReport::default();
|
let mut report = MediaRebuildReport::default();
|
||||||
let media_dir = data_dir.join("media");
|
let media_dir = data_dir.join("media");
|
||||||
@@ -356,8 +369,6 @@ pub fn rebuild_media_from_filesystem(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine if this is a translation sidecar: {name}.{lang}.meta
|
|
||||||
// where lang is exactly 2 lowercase letters.
|
|
||||||
if is_translation_sidecar(file_name) {
|
if is_translation_sidecar(file_name) {
|
||||||
translation_sidecars.push(path.to_path_buf());
|
translation_sidecars.push(path.to_path_buf());
|
||||||
} else {
|
} else {
|
||||||
@@ -365,8 +376,14 @@ pub fn rebuild_media_from_filesystem(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let total = canonical_sidecars.len() + translation_sidecars.len();
|
||||||
|
|
||||||
// Process canonical sidecars first
|
// 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) {
|
match rebuild_canonical_media(conn, data_dir, project_id, path) {
|
||||||
Ok(created) => {
|
Ok(created) => {
|
||||||
if created {
|
if created {
|
||||||
@@ -382,7 +399,12 @@ pub fn rebuild_media_from_filesystem(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Process translation sidecars
|
// 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) {
|
match rebuild_translation_sidecar(conn, data_dir, project_id, path) {
|
||||||
Ok(created) => {
|
Ok(created) => {
|
||||||
if created {
|
if created {
|
||||||
@@ -494,10 +516,9 @@ fn rebuild_canonical_media(
|
|||||||
sc.size
|
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 checksum = if abs_file.exists() {
|
||||||
let bytes = fs::read(&abs_file)?;
|
Some(crate::util::file_hash(&abs_file)?)
|
||||||
Some(content_hash(&bytes))
|
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -451,6 +451,19 @@ pub fn rebuild_posts_from_filesystem(
|
|||||||
conn: &Connection,
|
conn: &Connection,
|
||||||
data_dir: &Path,
|
data_dir: &Path,
|
||||||
project_id: &str,
|
project_id: &str,
|
||||||
|
) -> EngineResult<RebuildReport> {
|
||||||
|
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<dyn Fn(usize, usize, &str) + Send>;
|
||||||
|
|
||||||
|
/// 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<ItemProgressFn>,
|
||||||
) -> EngineResult<RebuildReport> {
|
) -> EngineResult<RebuildReport> {
|
||||||
let mut report = RebuildReport::default();
|
let mut report = RebuildReport::default();
|
||||||
let posts_dir = data_dir.join("posts");
|
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("");
|
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) {
|
if is_translation_filename(stem) {
|
||||||
translation_files.push(path.to_path_buf());
|
translation_files.push(path.to_path_buf());
|
||||||
} else {
|
} else {
|
||||||
@@ -486,8 +497,14 @@ pub fn rebuild_posts_from_filesystem(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let total = canonical_files.len() + translation_files.len();
|
||||||
|
|
||||||
// Process canonical posts first
|
// 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) {
|
match rebuild_canonical_post(conn, data_dir, project_id, path) {
|
||||||
Ok(created) => {
|
Ok(created) => {
|
||||||
if created {
|
if created {
|
||||||
@@ -503,7 +520,12 @@ pub fn rebuild_posts_from_filesystem(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Process translations
|
// 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) {
|
match rebuild_translation(conn, data_dir, project_id, path) {
|
||||||
Ok(created) => {
|
Ok(created) => {
|
||||||
if created {
|
if created {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use rusqlite::Connection;
|
use rusqlite::Connection;
|
||||||
|
|
||||||
@@ -27,6 +28,9 @@ pub struct FullRebuildReport {
|
|||||||
pub errors: Vec<String>,
|
pub errors: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Progress callback: (percent 0.0..1.0, phase description).
|
||||||
|
pub type ProgressFn = Arc<dyn Fn(f32, &str) + Send + Sync>;
|
||||||
|
|
||||||
/// Orchestrate a full rebuild from filesystem into the database.
|
/// Orchestrate a full rebuild from filesystem into the database.
|
||||||
///
|
///
|
||||||
/// Ensures FTS tables exist, then rebuilds posts, media, templates, and scripts
|
/// Ensures FTS tables exist, then rebuilds posts, media, templates, and scripts
|
||||||
@@ -35,42 +39,89 @@ pub fn rebuild_from_filesystem(
|
|||||||
conn: &Connection,
|
conn: &Connection,
|
||||||
data_dir: &Path,
|
data_dir: &Path,
|
||||||
project_id: &str,
|
project_id: &str,
|
||||||
|
) -> EngineResult<FullRebuildReport> {
|
||||||
|
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<ProgressFn>,
|
||||||
) -> EngineResult<FullRebuildReport> {
|
) -> EngineResult<FullRebuildReport> {
|
||||||
let mut report = FullRebuildReport::default();
|
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
|
// 1. Ensure FTS tables exist
|
||||||
|
progress(0.0, "Ensuring FTS tables...");
|
||||||
fts::ensure_fts_tables(conn)?;
|
fts::ensure_fts_tables(conn)?;
|
||||||
|
|
||||||
// 2. Rebuild posts
|
// 2. Rebuild posts (0.00 .. 0.35)
|
||||||
let post_report = post::rebuild_posts_from_filesystem(conn, data_dir, project_id)?;
|
progress(0.01, "Scanning posts...");
|
||||||
|
let post_item_cb: Option<post::ItemProgressFn> = 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_created = post_report.posts_created;
|
||||||
report.posts_updated = post_report.posts_updated;
|
report.posts_updated = post_report.posts_updated;
|
||||||
report.translations_created = post_report.translations_created;
|
report.translations_created = post_report.translations_created;
|
||||||
report.translations_updated = post_report.translations_updated;
|
report.translations_updated = post_report.translations_updated;
|
||||||
report.errors.extend(post_report.errors);
|
report.errors.extend(post_report.errors);
|
||||||
|
|
||||||
// 3. Rebuild media
|
// 3. Rebuild media (0.35 .. 0.70)
|
||||||
let media_report = media::rebuild_media_from_filesystem(conn, data_dir, project_id)?;
|
progress(0.35, "Scanning media...");
|
||||||
|
let media_item_cb: Option<media::ItemProgressFn> = 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_created = media_report.media_created;
|
||||||
report.media_updated = media_report.media_updated;
|
report.media_updated = media_report.media_updated;
|
||||||
report.media_translations_created = media_report.translations_created;
|
report.media_translations_created = media_report.translations_created;
|
||||||
report.media_translations_updated = media_report.translations_updated;
|
report.media_translations_updated = media_report.translations_updated;
|
||||||
report.errors.extend(media_report.errors);
|
report.errors.extend(media_report.errors);
|
||||||
|
|
||||||
// 4. Rebuild templates
|
// 4. Rebuild templates (0.70 .. 0.85)
|
||||||
|
progress(0.70, "Rebuilding templates...");
|
||||||
let tpl_report =
|
let tpl_report =
|
||||||
template_rebuild::rebuild_templates_from_filesystem(conn, data_dir, project_id)?;
|
template_rebuild::rebuild_templates_from_filesystem(conn, data_dir, project_id)?;
|
||||||
report.templates_created = tpl_report.created;
|
report.templates_created = tpl_report.created;
|
||||||
report.templates_updated = tpl_report.updated;
|
report.templates_updated = tpl_report.updated;
|
||||||
report.errors.extend(tpl_report.errors);
|
report.errors.extend(tpl_report.errors);
|
||||||
|
|
||||||
// 5. Rebuild scripts
|
// 5. Rebuild scripts (0.85 .. 1.0)
|
||||||
|
progress(0.85, "Rebuilding scripts...");
|
||||||
let script_report =
|
let script_report =
|
||||||
script_rebuild::rebuild_scripts_from_filesystem(conn, data_dir, project_id)?;
|
script_rebuild::rebuild_scripts_from_filesystem(conn, data_dir, project_id)?;
|
||||||
report.scripts_created = script_report.created;
|
report.scripts_created = script_report.created;
|
||||||
report.scripts_updated = script_report.updated;
|
report.scripts_updated = script_report.updated;
|
||||||
report.errors.extend(script_report.errors);
|
report.errors.extend(script_report.errors);
|
||||||
|
|
||||||
|
progress(1.0, "Rebuild complete");
|
||||||
Ok(report)
|
Ok(report)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
|
use std::io::Read;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
/// Compute a hex-encoded SHA-256 hash of the given content.
|
/// Compute a hex-encoded SHA-256 hash of the given content.
|
||||||
pub fn content_hash(content: &[u8]) -> String {
|
pub fn content_hash(content: &[u8]) -> String {
|
||||||
@@ -8,6 +10,21 @@ pub fn content_hash(content: &[u8]) -> String {
|
|||||||
hex::encode(result)
|
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<String> {
|
||||||
|
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.
|
// sha2 doesn't include hex encoding, so we use a tiny inline helper.
|
||||||
mod hex {
|
mod hex {
|
||||||
const HEX_CHARS: &[u8; 16] = b"0123456789abcdef";
|
const HEX_CHARS: &[u8; 16] = b"0123456789abcdef";
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ pub mod sidecar;
|
|||||||
pub mod thumbnail;
|
pub mod thumbnail;
|
||||||
|
|
||||||
pub use slug::{slugify, ensure_unique};
|
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 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 atomic_write::{atomic_write, atomic_write_str};
|
||||||
pub use paths::*;
|
pub use paths::*;
|
||||||
|
|||||||
@@ -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> {
|
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}"))?
|
.map_err(|e| format!("open: {e}"))?
|
||||||
.with_guessed_format()
|
.with_guessed_format()
|
||||||
.map_err(|e| format!("format: {e}"))?
|
.map_err(|e| format!("format: {e}"))?;
|
||||||
.decode()
|
let (w, h) = reader
|
||||||
.map_err(|e| format!("decode: {e}"))?;
|
.into_dimensions()
|
||||||
Ok(img.dimensions())
|
.map_err(|e| format!("dimensions: {e}"))?;
|
||||||
|
Ok((w, h))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Detect MIME type from file extension.
|
/// Detect MIME type from file extension.
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use bds_core::db::Database;
|
|||||||
use bds_core::engine::task::{TaskId, TaskManager, TaskStatus};
|
use bds_core::engine::task::{TaskId, TaskManager, TaskStatus};
|
||||||
use bds_core::engine;
|
use bds_core::engine;
|
||||||
use bds_core::i18n::{detect_os_locale, UiLocale};
|
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::i18n::{t, tw};
|
||||||
use crate::platform::menu::{self, MenuAction, MenuRegistry};
|
use crate::platform::menu::{self, MenuAction, MenuRegistry};
|
||||||
@@ -94,6 +94,13 @@ pub struct BdsApp {
|
|||||||
projects: Vec<Project>,
|
projects: Vec<Project>,
|
||||||
data_dir: Option<PathBuf>,
|
data_dir: Option<PathBuf>,
|
||||||
|
|
||||||
|
// Counts
|
||||||
|
post_count: usize,
|
||||||
|
media_count: usize,
|
||||||
|
|
||||||
|
// Sidebar data
|
||||||
|
sidebar_posts: Vec<Post>,
|
||||||
|
|
||||||
// Navigation
|
// Navigation
|
||||||
sidebar_view: SidebarView,
|
sidebar_view: SidebarView,
|
||||||
sidebar_visible: bool,
|
sidebar_visible: bool,
|
||||||
@@ -215,6 +222,9 @@ impl BdsApp {
|
|||||||
active_project: active_project.clone(),
|
active_project: active_project.clone(),
|
||||||
projects,
|
projects,
|
||||||
data_dir,
|
data_dir,
|
||||||
|
post_count: 0,
|
||||||
|
media_count: 0,
|
||||||
|
sidebar_posts: Vec::new(),
|
||||||
sidebar_view: SidebarView::Posts,
|
sidebar_view: SidebarView::Posts,
|
||||||
sidebar_visible: true,
|
sidebar_visible: true,
|
||||||
tabs: Vec::new(),
|
tabs: Vec::new(),
|
||||||
@@ -305,6 +315,7 @@ impl BdsApp {
|
|||||||
.and_then(|p| p.data_path.as_ref())
|
.and_then(|p| p.data_path.as_ref())
|
||||||
.map(PathBuf::from);
|
.map(PathBuf::from);
|
||||||
}
|
}
|
||||||
|
self.refresh_counts();
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
Message::SwitchProject(project_id) => {
|
Message::SwitchProject(project_id) => {
|
||||||
@@ -459,10 +470,14 @@ impl BdsApp {
|
|||||||
Message::RebuildDatabase => {
|
Message::RebuildDatabase => {
|
||||||
self.spawn_engine_task(
|
self.spawn_engine_task(
|
||||||
"engine.rebuildStarted",
|
"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 db = Database::open(&db_path).map_err(|e| e.to_string())?;
|
||||||
let report = engine::rebuild::rebuild_from_filesystem(db.conn(), &data_dir, &project_id)
|
let on_progress: engine::rebuild::ProgressFn = Arc::new(move |pct, msg| {
|
||||||
.map_err(|e| e.to_string())?;
|
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 posts = report.posts_created + report.posts_updated;
|
||||||
let media = report.media_created + report.media_updated;
|
let media = report.media_created + report.media_updated;
|
||||||
let templates = report.templates_created + report.templates_updated;
|
let templates = report.templates_created + report.templates_updated;
|
||||||
@@ -474,12 +489,14 @@ impl BdsApp {
|
|||||||
Message::ReindexText => {
|
Message::ReindexText => {
|
||||||
self.spawn_engine_task(
|
self.spawn_engine_task(
|
||||||
"engine.reindexStarted",
|
"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())?;
|
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)
|
let main_lang = engine::meta::read_project_json(&data_dir)
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|m| m.main_language)
|
.and_then(|m| m.main_language)
|
||||||
.unwrap_or_else(|| "en".to_string());
|
.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)
|
let report = engine::search::reindex_all(db.conn(), &project_id, &main_lang)
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
Ok(format!("posts={}, media={}", report.posts_indexed, report.media_indexed))
|
Ok(format!("posts={}, media={}", report.posts_indexed, report.media_indexed))
|
||||||
@@ -489,8 +506,9 @@ impl BdsApp {
|
|||||||
Message::RegenerateCalendar => {
|
Message::RegenerateCalendar => {
|
||||||
self.spawn_engine_task(
|
self.spawn_engine_task(
|
||||||
"engine.calendarStarted",
|
"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())?;
|
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)
|
engine::calendar::regenerate_calendar(db.conn(), &data_dir, &project_id)
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
Ok("done".to_string())
|
Ok("done".to_string())
|
||||||
@@ -501,8 +519,9 @@ impl BdsApp {
|
|||||||
self.open_singleton_tab(TabType::TranslationValidation, "Translation Validation");
|
self.open_singleton_tab(TabType::TranslationValidation, "Translation Validation");
|
||||||
self.spawn_engine_task(
|
self.spawn_engine_task(
|
||||||
"engine.validateTranslationsStarted",
|
"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())?;
|
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(
|
let report = engine::validate_translations::validate_translations(
|
||||||
db.conn(), &data_dir, &project_id,
|
db.conn(), &data_dir, &project_id,
|
||||||
).map_err(|e| e.to_string())?;
|
).map_err(|e| e.to_string())?;
|
||||||
@@ -513,8 +532,9 @@ impl BdsApp {
|
|||||||
Message::GenerateSite => {
|
Message::GenerateSite => {
|
||||||
self.spawn_engine_task(
|
self.spawn_engine_task(
|
||||||
"engine.generateSiteStarted",
|
"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())?;
|
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)
|
engine::calendar::regenerate_calendar(db.conn(), &data_dir, &project_id)
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
Ok("done".to_string())
|
Ok("done".to_string())
|
||||||
@@ -536,6 +556,7 @@ impl BdsApp {
|
|||||||
self.add_output(&format!("{label} failed: {err}"));
|
self.add_output(&format!("{label} failed: {err}"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
self.refresh_counts();
|
||||||
self.refresh_task_snapshots();
|
self.refresh_task_snapshots();
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
@@ -561,11 +582,12 @@ impl BdsApp {
|
|||||||
self.panel_tab,
|
self.panel_tab,
|
||||||
&self.task_snapshots,
|
&self.task_snapshots,
|
||||||
&self.output_entries,
|
&self.output_entries,
|
||||||
|
&self.sidebar_posts,
|
||||||
active_name,
|
active_name,
|
||||||
&self.projects,
|
&self.projects,
|
||||||
self.active_project.as_ref().map(|p| p.id.as_str()),
|
self.active_project.as_ref().map(|p| p.id.as_str()),
|
||||||
0, // post_count — populated in later milestones
|
self.post_count,
|
||||||
0, // media_count — populated in later milestones
|
self.media_count,
|
||||||
self.offline_mode,
|
self.offline_mode,
|
||||||
self.locale_dropdown_open,
|
self.locale_dropdown_open,
|
||||||
self.project_dropdown_open,
|
self.project_dropdown_open,
|
||||||
@@ -769,13 +791,18 @@ impl BdsApp {
|
|||||||
/// Returns `Task::none()` if no active project/db/data_dir.
|
/// Returns `Task::none()` if no active project/db/data_dir.
|
||||||
/// Otherwise registers the task, logs the start message, and returns an
|
/// Otherwise registers the task, logs the start message, and returns an
|
||||||
/// async `Task` that opens a fresh DB connection on a worker thread.
|
/// 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<F>(
|
fn spawn_engine_task<F>(
|
||||||
&mut self,
|
&mut self,
|
||||||
label_key: &str,
|
label_key: &str,
|
||||||
work: F,
|
work: F,
|
||||||
) -> Task<Message>
|
) -> Task<Message>
|
||||||
where
|
where
|
||||||
F: FnOnce(PathBuf, String, PathBuf) -> Result<String, String> + Send + 'static,
|
F: FnOnce(PathBuf, String, PathBuf, Arc<TaskManager>, TaskId) -> Result<String, String>
|
||||||
|
+ Send
|
||||||
|
+ 'static,
|
||||||
{
|
{
|
||||||
let (Some(project), Some(data_dir)) = (&self.active_project, &self.data_dir) else {
|
let (Some(project), Some(data_dir)) = (&self.active_project, &self.data_dir) else {
|
||||||
return Task::none();
|
return Task::none();
|
||||||
@@ -792,10 +819,11 @@ impl BdsApp {
|
|||||||
self.refresh_task_snapshots();
|
self.refresh_task_snapshots();
|
||||||
|
|
||||||
let label_for_msg = label.clone();
|
let label_for_msg = label.clone();
|
||||||
|
let tm = Arc::clone(&self.task_manager);
|
||||||
|
|
||||||
Task::perform(
|
Task::perform(
|
||||||
async move {
|
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
|
.await
|
||||||
.unwrap_or_else(|e| Err(format!("task panicked: {e}")))
|
.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();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,13 +106,19 @@ pub fn view(
|
|||||||
let items: Vec<Element<'static, Message>> = task_snapshots
|
let items: Vec<Element<'static, Message>> = task_snapshots
|
||||||
.iter()
|
.iter()
|
||||||
.map(|snap| {
|
.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!(
|
let status_text = format!(
|
||||||
"{} \u{2014} {}{}",
|
"{} \u{2014} {}{}{}",
|
||||||
snap.label,
|
snap.label,
|
||||||
snap.status,
|
snap.status,
|
||||||
snap.progress
|
progress_str,
|
||||||
.map(|p| format!(" ({:.0}%)", p * 100.0))
|
phase_str,
|
||||||
.unwrap_or_default(),
|
|
||||||
);
|
);
|
||||||
text(status_text).size(11).color(Color::from_rgb(0.70, 0.70, 0.75)).into()
|
text(status_text).size(11).color(Color::from_rgb(0.70, 0.70, 0.75)).into()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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 iced::{Background, Border, Color, Element, Length, Theme};
|
||||||
|
|
||||||
use bds_core::i18n::UiLocale;
|
use bds_core::i18n::UiLocale;
|
||||||
|
use bds_core::model::Post;
|
||||||
|
|
||||||
use crate::app::Message;
|
use crate::app::Message;
|
||||||
use crate::i18n::t;
|
use crate::i18n::t;
|
||||||
use crate::state::navigation::SidebarView;
|
use crate::state::navigation::SidebarView;
|
||||||
|
use crate::state::tabs::{Tab, TabType};
|
||||||
|
|
||||||
/// Sidebar container style — dark background with right border separator.
|
/// Sidebar container style — dark background with right border separator.
|
||||||
fn sidebar_style(_theme: &Theme) -> container::Style {
|
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.
|
/// Get the appropriate empty-state message key for each sidebar view.
|
||||||
fn placeholder_key(view: SidebarView) -> &'static str {
|
fn placeholder_key(view: SidebarView) -> &'static str {
|
||||||
match view {
|
match view {
|
||||||
@@ -38,19 +54,63 @@ fn placeholder_key(view: SidebarView) -> &'static str {
|
|||||||
|
|
||||||
pub fn view(
|
pub fn view(
|
||||||
sidebar_view: SidebarView,
|
sidebar_view: SidebarView,
|
||||||
|
posts: &[Post],
|
||||||
locale: UiLocale,
|
locale: UiLocale,
|
||||||
) -> Element<'static, Message> {
|
) -> Element<'static, Message> {
|
||||||
let header_text = t(locale, sidebar_view.i18n_key());
|
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<Element<'static, Message>> = 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![
|
let content = column![
|
||||||
text(header_text)
|
header,
|
||||||
.size(13)
|
|
||||||
.color(Color::from_rgb(0.85, 0.85, 0.90)),
|
|
||||||
Space::with_height(8.0),
|
Space::with_height(8.0),
|
||||||
text(placeholder)
|
body,
|
||||||
.size(12)
|
|
||||||
.color(Color::from_rgb(0.50, 0.50, 0.55)),
|
|
||||||
]
|
]
|
||||||
.spacing(4)
|
.spacing(4)
|
||||||
.padding(12);
|
.padding(12);
|
||||||
|
|||||||
@@ -129,12 +129,24 @@ pub fn view(
|
|||||||
.filter(|t| t.status == "running")
|
.filter(|t| t.status == "running")
|
||||||
.collect();
|
.collect();
|
||||||
let task_indicator: Element<'static, Message> = if !running.is_empty() {
|
let task_indicator: Element<'static, Message> = if !running.is_empty() {
|
||||||
let first = running[0].label.clone();
|
let first = &running[0];
|
||||||
if running.len() > 1 {
|
let progress_str = first.progress
|
||||||
text(format!("{first} (+{})", running.len() - 1)).size(11).color(label_color).into()
|
.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 {
|
} 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 {
|
} else {
|
||||||
Space::with_width(0).into()
|
Space::with_width(0).into()
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use iced::widget::text::Shaping;
|
|||||||
use iced::{Alignment, Background, Color, Element, Length, Padding, Theme};
|
use iced::{Alignment, Background, Color, Element, Length, Padding, Theme};
|
||||||
|
|
||||||
use bds_core::i18n::UiLocale;
|
use bds_core::i18n::UiLocale;
|
||||||
use bds_core::model::Project;
|
use bds_core::model::{Post, Project};
|
||||||
|
|
||||||
use crate::app::Message;
|
use crate::app::Message;
|
||||||
use crate::state::navigation::{OutputEntry, PanelTab, SidebarView, TaskSnapshot};
|
use crate::state::navigation::{OutputEntry, PanelTab, SidebarView, TaskSnapshot};
|
||||||
@@ -53,6 +53,8 @@ pub fn view(
|
|||||||
panel_tab: PanelTab,
|
panel_tab: PanelTab,
|
||||||
task_snapshots: &[TaskSnapshot],
|
task_snapshots: &[TaskSnapshot],
|
||||||
output_entries: &[OutputEntry],
|
output_entries: &[OutputEntry],
|
||||||
|
// Sidebar data
|
||||||
|
sidebar_posts: &[Post],
|
||||||
// Status bar
|
// Status bar
|
||||||
active_project_name: Option<&str>,
|
active_project_name: Option<&str>,
|
||||||
projects: &[Project],
|
projects: &[Project],
|
||||||
@@ -89,7 +91,7 @@ pub fn view(
|
|||||||
let mut main_row = row![activity];
|
let mut main_row = row![activity];
|
||||||
|
|
||||||
if sidebar_visible {
|
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());
|
main_row = main_row.push(separator_v());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user